perf(feed): relevance ranks the newest 2000 matches, the rest follow newest first
A broad word matched most of the library and ts_rank read every match: "a" took 4.4 s a page on prod. The window's start is read once (0.1 s) and rides the cursor, so a video ingested mid-scroll cannot repeat a row; the page is read as its two halves, each on an index (80 ms + 5 ms).
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"""Relevance ranks only the newest RANK_WINDOW matches (E31), DB-lane.
|
||||
|
||||
A broad word ("a") matched most of the prod library and ts_rank read every match's tsvector: 6.8 s a page.
|
||||
The ranking now covers the newest RANK_WINDOW matches and the rest follow newest first. The window's start
|
||||
rides the cursor, so a video ingested mid-scroll cannot push a row behind the cursor and repeat it.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from app import feedquery
|
||||
from app.models import Channel, Subscription, User, Video
|
||||
|
||||
|
||||
def _video(vid: str, day: int, title: str) -> Video:
|
||||
return Video(
|
||||
id=vid,
|
||||
channel_id="UCw",
|
||||
published_at=datetime(2024, 1, day, tzinfo=timezone.utc),
|
||||
title=title,
|
||||
is_short=False,
|
||||
live_status="none",
|
||||
via_search=False,
|
||||
via_explore=False,
|
||||
)
|
||||
|
||||
|
||||
def _seed(db) -> User:
|
||||
db.add(Channel(id="UCw", title="Window"))
|
||||
user = User(email="window@example.com", email_verified=True)
|
||||
db.add(user)
|
||||
db.flush()
|
||||
db.add(Subscription(user_id=user.id, channel_id="UCw", priority=0, hidden=False))
|
||||
# m1 is the oldest and the strongest match, m4 the strongest of the newest three; the rest tie on one
|
||||
# title "foo" each (ts_rank counts occurrences, it does not normalise by length).
|
||||
db.add_all(
|
||||
[
|
||||
_video("m1", 1, "foo foo foo foo foo"),
|
||||
_video("m2", 2, "foo and some other words here"),
|
||||
_video("m3", 3, "foo plus some filler words"),
|
||||
_video("m4", 4, "foo foo foo"),
|
||||
_video("m5", 5, "foo with a longer title"),
|
||||
_video("m6", 6, "foo with yet another title"),
|
||||
]
|
||||
)
|
||||
db.commit()
|
||||
return user
|
||||
|
||||
|
||||
def _ids(client, cursor=None, limit=50):
|
||||
url = f"/api/feed?q=foo&sort=relevance&limit={limit}"
|
||||
if cursor:
|
||||
url += f"&cursor={cursor}"
|
||||
resp = client.get(url)
|
||||
assert resp.status_code == 200, resp.text
|
||||
body = resp.json()
|
||||
return [it["id"] for it in body["items"]], body["next_cursor"]
|
||||
|
||||
|
||||
def test_a_specific_word_ranks_every_match(db, as_user):
|
||||
client = as_user(_seed(db))
|
||||
ids, _ = _ids(client)
|
||||
assert ids[:2] == ["m1", "m4"], ids
|
||||
|
||||
|
||||
def test_a_broad_word_ranks_the_newest_window_and_the_rest_follow_newest_first(
|
||||
db, as_user, monkeypatch
|
||||
):
|
||||
monkeypatch.setattr(feedquery, "RANK_WINDOW", 3)
|
||||
client = as_user(_seed(db))
|
||||
ids, _ = _ids(client)
|
||||
assert ids[0] == "m4", ids # the strongest of the newest three
|
||||
assert sorted(ids[:3]) == ["m4", "m5", "m6"], ids
|
||||
assert ids[3:] == ["m3", "m2", "m1"], ids # past the window: newest first, m1 not ranked
|
||||
|
||||
|
||||
def test_paging_keeps_the_window_page_one_ranked_against(db, as_user, monkeypatch):
|
||||
monkeypatch.setattr(feedquery, "RANK_WINDOW", 3)
|
||||
client = as_user(_seed(db))
|
||||
full, _ = _ids(client)
|
||||
|
||||
first, cursor = _ids(client, limit=2)
|
||||
assert first == full[:2]
|
||||
# A newer match lands mid-scroll. Ranked against a recomputed window, m4 (shown on page one) would
|
||||
# fall out of it to rank 0, sort behind the cursor and come back on page two.
|
||||
db.add(_video("m7", 7, "foo arriving while the reader scrolls"))
|
||||
db.commit()
|
||||
|
||||
walked = list(first)
|
||||
while cursor:
|
||||
page, cursor = _ids(client, cursor, limit=2)
|
||||
walked.extend(page)
|
||||
assert len(walked) == len(set(walked)), walked
|
||||
assert set(full) <= set(walked), walked
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("window", "limit"),
|
||||
# 3: the page is read as its two halves (the window ranked, the rest newest first), and pages of 2
|
||||
# and 3 cross the window's edge mid-page and exactly at it. 2000: one ranked query.
|
||||
[(3, 2), (3, 3), (2000, 2)],
|
||||
)
|
||||
def test_cursor_walk_matches_the_unpaged_order(db, as_user, monkeypatch, window, limit):
|
||||
monkeypatch.setattr(feedquery, "RANK_WINDOW", window)
|
||||
client = as_user(_seed(db))
|
||||
full, _ = _ids(client)
|
||||
if window == 3:
|
||||
assert full == ["m4", "m6", "m5", "m3", "m2", "m1"]
|
||||
walked, cursor = _ids(client, limit=limit)
|
||||
while cursor:
|
||||
page, cursor = _ids(client, cursor, limit=limit)
|
||||
walked.extend(page)
|
||||
assert walked == full
|
||||
Reference in New Issue
Block a user