Files
mvanhorn--last30days-skill/tests/test_reddit_relevance_ranking.py
rzachmith d6a4dd0822 fix(reddit): relevance floor + relevance-first ranking so viral off-topic posts don't dominate (#488)
* fix(reddit): relevance floor + relevance-first ranking

Reddit results were dominated by high-upvote, off-topic content
(relationship drama, AITA, viral news) on most queries. Both the keyed
(ScrapeCreators) and keyless (RSS) paths ranked the final list
engagement-first with no relevance floor, so a 10k-upvote post with zero
topic overlap outranked on-topic posts.

- Add a relevance floor: drop zero-overlap posts (relevance == 0) when
  anything relevant remains, so viral junk can't fill the section.
- Rank relevance-first with a bounded (<= 0.25) log-scaled engagement
  tiebreaker, so an off-topic viral post can never outrank an on-topic one.
- Apply to both reddit.py (Phase 6) and reddit_keyless.py. Composes with
  the #484 enrichment-slot work: slot selection is unchanged; the final
  display order is now relevance-correct.
- Add tests/test_reddit_relevance_ranking.py.

Follow-up to #484.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(reddit): address Greptile review nits

- Move RELEVANCE_FLOOR / MIN_ON_TOPIC to relevance.py as the single source
  of truth; keyed and keyless paths now import them instead of redefining,
  so they can't silently diverge.
- Move _relevance_rank_key below _total_engagement in reddit.py so it no
  longer forward-references a function defined ~220 lines later.
- Log the dropped-post count in the keyless relevance floor, matching the
  observability the keyed path already had.

No behavior change. Follow-up to review on #488.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 12:35:18 -07:00

107 lines
4.9 KiB
Python

"""Tests for relevance-floor + relevance-first ranking in the Reddit paths.
Reddit's highest-upvote content (relationship drama, AITA, viral news) often
has near-zero topic overlap. Before this change both the keyed (ScrapeCreators)
and keyless (RSS) paths ranked the final list engagement-first, so a viral
off-topic post outranked on-topic posts. These tests pin the new behavior:
on-topic posts rank first and pure zero-overlap posts are dropped when anything
relevant remains.
"""
from unittest import mock
from lib import reddit, reddit_keyless
# --------------------------------------------------------------------------- #
# Shared ranking key
# --------------------------------------------------------------------------- #
class TestRelevanceRankKey:
def test_on_topic_low_upvote_beats_off_topic_viral(self):
on_topic = {"relevance": 0.3, "engagement": {"score": 10, "num_comments": 5}}
off_topic = {"relevance": 0.0, "engagement": {"score": 99999, "num_comments": 4000}}
assert reddit._relevance_rank_key(on_topic) > reddit._relevance_rank_key(off_topic)
def test_engagement_bonus_is_bounded(self):
# Even astronomical engagement adds at most 0.25, so it can never lift a
# relevance-0 post above a post that cleared the floor.
huge = {"relevance": 0.0, "engagement": {"score": 10**9, "num_comments": 10**9}}
assert reddit._relevance_rank_key(huge) <= 0.25 + 1e-9
floored = {"relevance": 0.3, "engagement": {"score": 0, "num_comments": 0}}
assert reddit._relevance_rank_key(floored) > reddit._relevance_rank_key(huge)
def test_keyless_key_matches_keyed_semantics(self):
on_topic = {"relevance": 0.3, "engagement": {"score": 10, "num_comments": 5}}
off_topic = {"relevance": 0.0, "engagement": {"score": 99999, "num_comments": 4000}}
assert reddit_keyless._relevance_rank_key(on_topic) > reddit_keyless._relevance_rank_key(off_topic)
# --------------------------------------------------------------------------- #
# Keyed path (reddit.search_reddit)
# --------------------------------------------------------------------------- #
def _raw(rid, title, ups, sub):
return {
"id": f"t3_{rid}",
"title": title,
"selftext": "",
"permalink": f"/r/{sub}/comments/{rid}/post/",
"subreddit": sub,
"created_utc": 1716000000, # 2024-05-18, kept by a wide date range
"ups": ups,
"num_comments": max(1, ups // 10),
}
class TestKeyedRanking:
def test_on_topic_outranks_viral_and_zero_overlap_dropped(self):
topic = "electric vehicle home charging"
on_topic = _raw("aaa", "Electric vehicle home charging setup guide", 5, "electricvehicles")
viral = _raw("bbb", "AITA for not sharing my lottery winnings", 99999, "AmItheAsshole")
with mock.patch.object(reddit, "_global_search", return_value=[on_topic, viral]), \
mock.patch.object(reddit, "_subreddit_search", return_value=[]):
result = reddit.search_reddit(topic, "2000-01-01", "2100-01-01", depth="default", token="x")
items = result["items"]
urls = [it["url"] for it in items]
# Zero-overlap viral post is stripped because an on-topic post exists.
assert any("electricvehicles" in u for u in urls)
assert not any("AmItheAsshole" in u for u in urls)
# On-topic post leads.
assert "electricvehicles" in items[0]["url"]
# --------------------------------------------------------------------------- #
# Keyless path (reddit_keyless.search_and_enrich)
# --------------------------------------------------------------------------- #
def _kpost(rid, rel, score, date="2026-05-20"):
return {
"id": "", "title": f"Post {rid}", "url": f"https://www.reddit.com/r/t/comments/{rid}/p/",
"score": score, "num_comments": score, "subreddit": "t", "created_utc": None,
"author": "u", "selftext": "", "date": date,
"engagement": {"score": score, "num_comments": score, "upvote_ratio": None},
"relevance": rel, "why_relevant": "Reddit RSS", "metadata": {},
}
class TestKeylessRanking:
def test_relevance_first_and_zero_overlap_dropped(self):
on_strong = _kpost("aaa", 0.5, 10)
on_weak = _kpost("bbb", 0.2, 5000)
off_viral = _kpost("ccc", 0.0, 99999)
with mock.patch.object(reddit_keyless, "_discover",
return_value=[off_viral, on_weak, on_strong]), \
mock.patch.object(reddit_keyless, "_enrich", side_effect=lambda posts, depth: posts):
out = reddit_keyless.search_and_enrich(
"some topic", "2026-05-07", "2026-06-06", depth="default")
urls = [p["url"] for p in out]
# Zero-overlap viral post dropped; on-topic posts kept, strongest first.
assert "ccc" not in "".join(urls)
assert out[0]["url"].endswith("/aaa/p/")
assert len(out) == 2