Tighten relevance scoring and Polymarket ranking

Score against original user intent on Reddit, remove the artificial low-end relevance floor, and make Polymarket semantics dominate generic market quality signals.

Also apply the relevance filter to Polymarket and update the affected cross-source tests.

Validation: uv run python -m unittest
This commit is contained in:
Jeffrey Sperling
2026-03-13 19:21:25 -07:00
parent cbee987f65
commit 946af84f9a
12 changed files with 147 additions and 61 deletions
+1
View File
@@ -1838,6 +1838,7 @@ def main():
deduped_hn = score.relevance_filter(deduped_hn, "HN") deduped_hn = score.relevance_filter(deduped_hn, "HN")
deduped_bsky = score.relevance_filter(deduped_bsky, "BLUESKY") deduped_bsky = score.relevance_filter(deduped_bsky, "BLUESKY")
deduped_ts = score.relevance_filter(deduped_ts, "TRUTHSOCIAL") deduped_ts = score.relevance_filter(deduped_ts, "TRUTHSOCIAL")
deduped_pm = score.relevance_filter(deduped_pm, "POLYMARKET") if deduped_pm else []
# Cross-source linking: annotate items that discuss the same story # Cross-source linking: annotate items that discuss the same story
dedupe.cross_source_link( dedupe.cross_source_link(
+16 -24
View File
@@ -13,6 +13,7 @@ from typing import Any, Dict, List, Optional
from urllib.parse import quote_plus, urlencode from urllib.parse import quote_plus, urlencode
from . import http from . import http
from .relevance import token_overlap_relevance
GAMMA_SEARCH_URL = "https://gamma-api.polymarket.com/public-search" GAMMA_SEARCH_URL = "https://gamma-api.polymarket.com/public-search"
@@ -314,8 +315,8 @@ def _shorten_question(question: str) -> str:
def _compute_text_similarity(topic: str, title: str, outcomes: List[str] = None) -> float: def _compute_text_similarity(topic: str, title: str, outcomes: List[str] = None) -> float:
"""Score how well the event title (or outcome names) match the search topic. """Score how well the event title (or outcome names) match the search topic.
Returns 0.0-1.0. Title substring match gets 1.0, outcome match gets 0.85/0.7, Returns 0.0-1.0. Exact title phrase match gets 1.0. Otherwise we reuse the
title token overlap gets proportional score. shared query-centric relevance scorer and take the best title/outcome match.
""" """
core = _extract_core_subject(topic).lower() core = _extract_core_subject(topic).lower()
title_lower = title.lower() title_lower = title.lower()
@@ -326,27 +327,17 @@ def _compute_text_similarity(topic: str, title: str, outcomes: List[str] = None)
if core in title_lower: if core in title_lower:
return 1.0 return 1.0
# Check if topic appears in any outcome name (bidirectional) best_score = token_overlap_relevance(core, title)
if outcomes: if outcomes:
core_tokens = set(core.split())
best_outcome_score = 0.0
for outcome_name in outcomes: for outcome_name in outcomes:
outcome_lower = outcome_name.lower() outcome_lower = outcome_name.lower()
# Bidirectional: "arizona" in "arizona basketball" OR "arizona basketball" contains "arizona" outcome_score = token_overlap_relevance(core, outcome_name)
if core in outcome_lower or outcome_lower in core: if core in outcome_lower or outcome_lower in core:
best_outcome_score = max(best_outcome_score, 0.85) outcome_score = max(outcome_score, 0.92 if len(outcome_lower.split()) >= 2 else 0.88)
elif core_tokens & set(outcome_lower.split()): best_score = max(best_score, outcome_score)
best_outcome_score = max(best_outcome_score, 0.7)
if best_outcome_score > 0:
return best_outcome_score
# Token overlap fallback against title return round(best_score, 2)
topic_tokens = set(core.split())
title_tokens = set(title_lower.split())
if not topic_tokens:
return 0.5
overlap = len(topic_tokens & title_tokens)
return overlap / len(topic_tokens)
def _safe_float(val, default=0.0) -> float: def _safe_float(val, default=0.0) -> float:
@@ -484,7 +475,8 @@ def parse_polymarket_response(response: Dict[str, Any], topic: str = "") -> List
except (IndexError, TypeError): except (IndexError, TypeError):
end_date = None end_date = None
# Quality-signal relevance (replaces position-based decay) # Semantic relevance should dominate. Market quality should refine
# relevant matches, not rescue unrelated high-liquidity events.
text_score = _compute_text_similarity(topic, title, all_outcome_names) if topic else 0.5 text_score = _compute_text_similarity(topic, title, all_outcome_names) if topic else 0.5
# Volume signal: log-scaled monthly volume (most stable signal) # Volume signal: log-scaled monthly volume (most stable signal)
@@ -504,13 +496,13 @@ def parse_polymarket_response(response: Dict[str, Any], topic: str = "") -> List
# Competitive bonus: markets near 50/50 are more interesting # Competitive bonus: markets near 50/50 are more interesting
competitive_score = event_competitive competitive_score = event_competitive
relevance = min(1.0, ( market_quality = (
0.30 * text_score + 0.50 * vol_score +
0.30 * vol_score + 0.25 * liq_score +
0.15 * liq_score +
0.15 * movement_score + 0.15 * movement_score +
0.10 * competitive_score 0.10 * competitive_score
)) )
relevance = min(1.0, text_score * (0.75 + 0.25 * market_quality))
# Surface the topic-matching outcome to the front before truncating # Surface the topic-matching outcome to the front before truncating
if topic and outcome_prices: if topic and outcome_prices:
+5 -3
View File
@@ -108,12 +108,14 @@ def expand_reddit_queries(topic: str, depth: str) -> List[str]:
if core.lower() != original_clean.lower() and len(original_clean.split()) <= 8: if core.lower() != original_clean.lower() and len(original_clean.split()) <= 8:
queries.append(original_clean) queries.append(original_clean)
# Add opinion/review variant except for how_to/comparison queries # Opinion/review variants help mostly for product and opinion queries.
# They contaminate broader searches like predictions or breaking news.
qtype = detect_query_type(topic) qtype = detect_query_type(topic)
if depth in ("default", "deep") and qtype not in ("how_to", "comparison"): if depth in ("default", "deep") and qtype in ("product", "opinion"):
queries.append(f"{core} worth it OR thoughts OR review") queries.append(f"{core} worth it OR thoughts OR review")
if depth == "deep": # Problem/bug variants are useful for tool workflows, not generic news.
if depth == "deep" and qtype in ("product", "opinion", "how_to"):
queries.append(f"{core} issues OR problems OR bug OR broken") queries.append(f"{core} issues OR problems OR bug OR broken")
return queries return queries
+62 -8
View File
@@ -1,6 +1,9 @@
"""Shared token-overlap relevance scoring for search result ranking. """Shared token-overlap relevance scoring for search result ranking.
Tokenizes text, expands synonyms, and computes query-to-content overlap ratios. The score is intentionally query-centric:
- exact phrase matches should score very high
- partial matches should pay a meaningful penalty
- matches on generic words alone ("odds", "review") should not pass as relevant
""" """
import re import re
@@ -36,6 +39,18 @@ SYNONYMS = {
'vuejs': {'vue'}, 'vuejs': {'vue'},
} }
# Generic query words that should not carry relevance on their own.
# They still help when paired with stronger entity/topic matches.
LOW_SIGNAL_QUERY_TOKENS = frozenset({
'advice', 'animation', 'animations', 'best', 'chance', 'chances',
'code', 'compare', 'comparison', 'differences', 'explain', 'guide',
'guides', 'how', 'latest', 'news', 'odds', 'opinion', 'opinions',
'prediction', 'predictions', 'probability', 'probabilities', 'prompt',
'prompting', 'prompts', 'rate', 'review', 'reviews', 'thoughts',
'tip', 'tips', 'tutorial', 'tutorials', 'update', 'updates', 'use',
'using', 'versus', 'vs', 'worth',
})
def tokenize(text: str) -> Set[str]: def tokenize(text: str) -> Set[str]:
"""Lowercase, strip punctuation, remove stopwords, drop single-char tokens. """Lowercase, strip punctuation, remove stopwords, drop single-char tokens.
@@ -51,15 +66,25 @@ def tokenize(text: str) -> Set[str]:
return expanded return expanded
def _normalize_phrase(text: str) -> str:
"""Normalize text for phrase containment checks."""
return ' '.join(re.sub(r'[^\w\s]', ' ', text.lower()).split())
def token_overlap_relevance( def token_overlap_relevance(
query: str, query: str,
text: str, text: str,
hashtags: Optional[List[str]] = None, hashtags: Optional[List[str]] = None,
) -> float: ) -> float:
"""Compute relevance as ratio of query tokens found in text. """Compute a query-centric relevance score between 0.0 and 1.0.
Uses ratio overlap (intersection / query_length) so short queries The score combines:
score higher when fully represented in the text. Floors at 0.1. - query coverage
- informative-token coverage
- a small precision term to penalize extra noise
- an exact phrase bonus
Generic tokens alone are capped below the post-retrieval 0.3 threshold.
Args: Args:
query: Search query query: Search query
@@ -68,7 +93,7 @@ def token_overlap_relevance(
hashtags are split to match query tokens (e.g. "claudecode" matches "claude"). hashtags are split to match query tokens (e.g. "claudecode" matches "claude").
Returns: Returns:
Float between 0.1 and 1.0 (0.5 for empty queries) Float between 0.0 and 1.0 (0.5 for empty queries)
""" """
q_tokens = tokenize(query) q_tokens = tokenize(query)
@@ -89,6 +114,35 @@ def token_overlap_relevance(
if not q_tokens: if not q_tokens:
return 0.5 # Neutral fallback for empty/stopword-only queries return 0.5 # Neutral fallback for empty/stopword-only queries
overlap = len(q_tokens & t_tokens) overlap_tokens = q_tokens & t_tokens
ratio = overlap / len(q_tokens) overlap = len(overlap_tokens)
return max(0.1, min(1.0, ratio)) if overlap == 0:
return 0.0
informative_q_tokens = {t for t in q_tokens if t not in LOW_SIGNAL_QUERY_TOKENS}
if not informative_q_tokens:
informative_q_tokens = q_tokens
coverage = overlap / len(q_tokens)
informative_overlap = len(informative_q_tokens & t_tokens) / len(informative_q_tokens)
precision_denominator = min(len(t_tokens), len(q_tokens) + 4) or 1
precision = overlap / precision_denominator
phrase_bonus = 0.0
normalized_query = _normalize_phrase(query)
normalized_text = _normalize_phrase(combined)
if normalized_query and normalized_query in normalized_text:
phrase_bonus = 0.12 if len(normalized_query.split()) > 1 else 0.16
base = (
0.55 * (coverage ** 1.35) +
0.25 * informative_overlap +
0.20 * precision
)
# If we only matched generic query words, keep the score below the
# normal relevance filter threshold so these do not survive by default.
if informative_q_tokens and not (informative_q_tokens & t_tokens):
return round(min(0.24, base), 2)
return round(min(1.0, base + phrase_bonus), 2)
+9 -3
View File
@@ -11,6 +11,12 @@ WEIGHT_RELEVANCE = 0.45
WEIGHT_RECENCY = 0.25 WEIGHT_RECENCY = 0.25
WEIGHT_ENGAGEMENT = 0.30 WEIGHT_ENGAGEMENT = 0.30
# Polymarket needs stronger semantic weighting because volume/liquidity already
# show up as engagement and lightly influence parse-time relevance.
PM_WEIGHT_RELEVANCE = 0.60
PM_WEIGHT_RECENCY = 0.20
PM_WEIGHT_ENGAGEMENT = 0.20
# WebSearch weights (no engagement data available) # WebSearch weights (no engagement data available)
WEBSEARCH_WEIGHT_RELEVANCE = 0.55 WEBSEARCH_WEIGHT_RELEVANCE = 0.55
WEBSEARCH_WEIGHT_RECENCY = 0.45 WEBSEARCH_WEIGHT_RECENCY = 0.45
@@ -632,9 +638,9 @@ def score_polymarket_items(items: List[schema.PolymarketItem]) -> List[schema.Po
) )
overall = ( overall = (
WEIGHT_RELEVANCE * rel_score + PM_WEIGHT_RELEVANCE * rel_score +
WEIGHT_RECENCY * rec_score + PM_WEIGHT_RECENCY * rec_score +
WEIGHT_ENGAGEMENT * eng_score PM_WEIGHT_ENGAGEMENT * eng_score
) )
if eng_raw[i] is None: if eng_raw[i] is None:
+2 -2
View File
@@ -57,9 +57,9 @@ class TestComputeRelevance(unittest.TestCase):
boosted = instagram._compute_relevance("claude code", "random video about stuff", ["claudecode", "ai"]) boosted = instagram._compute_relevance("claude code", "random video about stuff", ["claudecode", "ai"])
self.assertGreater(boosted, base) self.assertGreater(boosted, base)
def test_floor_at_01(self): def test_no_match_returns_zero(self):
rel = instagram._compute_relevance("quantum physics", "cat dancing video") rel = instagram._compute_relevance("quantum physics", "cat dancing video")
self.assertGreaterEqual(rel, 0.1) self.assertEqual(rel, 0.0)
def test_empty_query_returns_default(self): def test_empty_query_returns_default(self):
rel = instagram._compute_relevance("", "Some video title") rel = instagram._compute_relevance("", "Some video title")
+17 -7
View File
@@ -569,8 +569,9 @@ class TestTextSimilarity(unittest.TestCase):
def test_partial_token_overlap(self): def test_partial_token_overlap(self):
score = polymarket._compute_text_similarity("Arizona Basketball", "Will Arizona win?") score = polymarket._compute_text_similarity("Arizona Basketball", "Will Arizona win?")
# "Arizona" matches, "Basketball" doesn't -> 0.5 # Partial informative match should stay below exact match.
self.assertAlmostEqual(score, 0.5) self.assertGreater(score, 0.3)
self.assertLess(score, 0.6)
def test_no_overlap(self): def test_no_overlap(self):
score = polymarket._compute_text_similarity("Arizona Basketball", "Will AI regulation pass?") score = polymarket._compute_text_similarity("Arizona Basketball", "Will AI regulation pass?")
@@ -595,7 +596,7 @@ class TestTextSimilarity(unittest.TestCase):
"Who will be the #1 overall seed?", "Who will be the #1 overall seed?",
outcomes=["Duke", "Arizona", "Houston"], outcomes=["Duke", "Arizona", "Houston"],
) )
self.assertEqual(score, 0.85) self.assertEqual(score, 1.0)
def test_outcome_bidirectional_match(self): def test_outcome_bidirectional_match(self):
"""Topic 'Arizona Basketball' should match outcome 'Arizona' (outcome in core).""" """Topic 'Arizona Basketball' should match outcome 'Arizona' (outcome in core)."""
@@ -604,16 +605,17 @@ class TestTextSimilarity(unittest.TestCase):
"Who will be the #1 overall seed?", "Who will be the #1 overall seed?",
outcomes=["Duke", "Arizona", "Houston"], outcomes=["Duke", "Arizona", "Houston"],
) )
self.assertEqual(score, 0.85) self.assertEqual(score, 0.88)
def test_outcome_token_overlap(self): def test_outcome_token_overlap(self):
"""Partial token overlap with outcome gets 0.7 when no substring match.""" """Partial token overlap with outcome gets a moderate score."""
score = polymarket._compute_text_similarity( score = polymarket._compute_text_similarity(
"Iran War", "Iran War",
"Unrelated geopolitics title", "Unrelated geopolitics title",
outcomes=["War continues", "Peace deal"], outcomes=["War continues", "Peace deal"],
) )
self.assertEqual(score, 0.7) self.assertGreater(score, 0.3)
self.assertLess(score, 0.6)
def test_outcome_no_match(self): def test_outcome_no_match(self):
"""No outcome match falls through to title token overlap.""" """No outcome match falls through to title token overlap."""
@@ -632,7 +634,15 @@ class TestTextSimilarity(unittest.TestCase):
"Unrelated title", "Unrelated title",
outcomes=["Arizona"], outcomes=["Arizona"],
) )
self.assertEqual(score, 0.85) self.assertEqual(score, 1.0)
def test_generic_only_odds_match_stays_below_threshold(self):
score = polymarket._compute_text_similarity(
"Anthropic odds",
"Republican 2026 House odds",
outcomes=["Yes", "No"],
)
self.assertLess(score, 0.3)
def test_title_match_still_beats_outcome(self): def test_title_match_still_beats_outcome(self):
"""Title substring match (1.0) takes priority over outcome match (0.85).""" """Title substring match (1.0) takes priority over outcome match (0.85)."""
+16 -4
View File
@@ -47,16 +47,28 @@ class TestExpandRedditQueries(unittest.TestCase):
self.assertGreaterEqual(len(queries), 1) self.assertGreaterEqual(len(queries), 1)
def test_default_includes_review_variant(self): def test_default_includes_review_variant(self):
queries = reddit.expand_reddit_queries("cursor IDE", "default") queries = reddit.expand_reddit_queries("cursor IDE pricing", "default")
self.assertTrue(any("worth it" in q or "review" in q for q in queries)) self.assertTrue(any("worth it" in q or "review" in q for q in queries))
def test_default_skips_review_variant_for_prediction(self):
queries = reddit.expand_reddit_queries("anthropic odds", "default")
self.assertFalse(any("worth it" in q or "review" in q for q in queries))
def test_default_skips_review_variant_for_breaking_news(self):
queries = reddit.expand_reddit_queries("kanye west", "default")
self.assertFalse(any("worth it" in q or "review" in q for q in queries))
def test_deep_includes_issues_variant(self): def test_deep_includes_issues_variant(self):
queries = reddit.expand_reddit_queries("cursor IDE", "deep") queries = reddit.expand_reddit_queries("cursor IDE pricing", "deep")
self.assertTrue(any("issues" in q or "problems" in q for q in queries)) self.assertTrue(any("issues" in q or "problems" in q for q in queries))
def test_deep_skips_issues_variant_for_prediction(self):
queries = reddit.expand_reddit_queries("anthropic odds", "deep")
self.assertFalse(any("issues" in q or "problems" in q for q in queries))
def test_deep_has_more_queries_than_quick(self): def test_deep_has_more_queries_than_quick(self):
quick = reddit.expand_reddit_queries("cursor IDE", "quick") quick = reddit.expand_reddit_queries("cursor IDE pricing", "quick")
deep = reddit.expand_reddit_queries("cursor IDE", "deep") deep = reddit.expand_reddit_queries("cursor IDE pricing", "deep")
self.assertGreater(len(deep), len(quick)) self.assertGreater(len(deep), len(quick))
+10 -1
View File
@@ -75,7 +75,7 @@ class TestTokenOverlapRelevance(unittest.TestCase):
def test_floor_at_0_1(self): def test_floor_at_0_1(self):
rel = token_overlap_relevance("quantum physics", "cat dancing video") rel = token_overlap_relevance("quantum physics", "cat dancing video")
self.assertGreaterEqual(rel, 0.1) self.assertEqual(rel, 0.0)
def test_full_match_returns_1(self): def test_full_match_returns_1(self):
rel = token_overlap_relevance("python tutorial", "Python Tutorial for Beginners") rel = token_overlap_relevance("python tutorial", "Python Tutorial for Beginners")
@@ -96,6 +96,15 @@ class TestTokenOverlapRelevance(unittest.TestCase):
rel = token_overlap_relevance("the a is", "some content here") rel = token_overlap_relevance("the a is", "some content here")
self.assertEqual(rel, 0.5) self.assertEqual(rel, 0.5)
def test_generic_only_overlap_stays_below_filter_threshold(self):
rel = token_overlap_relevance("anthropic odds", "Republican house odds update")
self.assertLess(rel, 0.3)
def test_informative_partial_match_stays_above_generic_only(self):
generic_only = token_overlap_relevance("anthropic odds", "Republican house odds update")
informative = token_overlap_relevance("anthropic odds", "Anthropic valuation market")
self.assertGreater(informative, generic_only)
class TestHashtagRelevance(unittest.TestCase): class TestHashtagRelevance(unittest.TestCase):
"""Tests for hashtag-aware relevance (TikTok/Instagram pattern).""" """Tests for hashtag-aware relevance (TikTok/Instagram pattern)."""
+2 -2
View File
@@ -44,9 +44,9 @@ class TestComputeRelevance(unittest.TestCase):
score = scrapecreators_x._compute_relevance("", "some text") score = scrapecreators_x._compute_relevance("", "some text")
self.assertEqual(score, 0.5) self.assertEqual(score, 0.5)
def test_floor_at_01(self): def test_no_match_returns_zero(self):
score = scrapecreators_x._compute_relevance("abcdef ghijkl", "xyz") score = scrapecreators_x._compute_relevance("abcdef ghijkl", "xyz")
self.assertGreaterEqual(score, 0.1) self.assertEqual(score, 0.0)
class TestExtractCoreSubject(unittest.TestCase): class TestExtractCoreSubject(unittest.TestCase):
+2 -2
View File
@@ -33,9 +33,9 @@ class TestTikTokRelevance(unittest.TestCase):
rel = tiktok._compute_relevance("", "Some video title") rel = tiktok._compute_relevance("", "Some video title")
self.assertEqual(rel, 0.5) self.assertEqual(rel, 0.5)
def test_floor(self): def test_no_match_returns_zero(self):
rel = tiktok._compute_relevance("quantum physics", "cat dancing video") rel = tiktok._compute_relevance("quantum physics", "cat dancing video")
self.assertGreaterEqual(rel, 0.1) self.assertEqual(rel, 0.0)
class TestExtractCoreSubject(unittest.TestCase): class TestExtractCoreSubject(unittest.TestCase):
+5 -5
View File
@@ -62,7 +62,7 @@ class TestComputeRelevance(unittest.TestCase):
def test_no_match(self): def test_no_match(self):
result = _compute_relevance("Claude Code", "Python Web Scraping") result = _compute_relevance("Claude Code", "Python Web Scraping")
self.assertEqual(result, 0.1) # Floor self.assertEqual(result, 0.0)
def test_empty_query_returns_neutral(self): def test_empty_query_returns_neutral(self):
result = _compute_relevance("", "Some Video Title") result = _compute_relevance("", "Some Video Title")
@@ -74,7 +74,7 @@ class TestComputeRelevance(unittest.TestCase):
def test_empty_title(self): def test_empty_title(self):
result = _compute_relevance("Claude Code", "") result = _compute_relevance("Claude Code", "")
self.assertEqual(result, 0.1) # Floor self.assertEqual(result, 0.0)
def test_case_insensitive(self): def test_case_insensitive(self):
result = _compute_relevance("claude code", "CLAUDE CODE Tutorial") result = _compute_relevance("claude code", "CLAUDE CODE Tutorial")
@@ -89,9 +89,9 @@ class TestComputeRelevance(unittest.TestCase):
) )
self.assertEqual(result, 1.0) self.assertEqual(result, 1.0)
def test_floor_at_0_1(self): def test_no_match_returns_zero(self):
result = _compute_relevance("quantum computing", "cat videos compilation") result = _compute_relevance("quantum computing", "cat videos compilation")
self.assertEqual(result, 0.1) self.assertEqual(result, 0.0)
def test_cap_at_1_0(self): def test_cap_at_1_0(self):
result = _compute_relevance("AI", "AI AI AI AI AI") result = _compute_relevance("AI", "AI AI AI AI AI")
@@ -103,7 +103,7 @@ class TestComputeRelevance(unittest.TestCase):
def test_single_word_no_match(self): def test_single_word_no_match(self):
result = _compute_relevance("Seedance", "Random cooking video") result = _compute_relevance("Seedance", "Random cooking video")
self.assertEqual(result, 0.1) self.assertEqual(result, 0.0)
if __name__ == "__main__": if __name__ == "__main__":