fix(rerank): ground entity-miss demotion on head token, not full phrase
The entity-grounding demotion required the full multi-word primary entity as a contiguous substring, so on-entity items missing a trailing search descriptor were buried: a 323-pt HN thread "Stripe is friendly to 'friendly fraud'" scored 0 on a "Stripe payments" query. New _entity_grounded helper keys on the brand head token; items that never name the brand still miss it and stay demoted. reddit_keyless _slot_priority, which had re-implemented the old check while claiming to mirror rerank's signal, now calls the shared helper so the two paths cannot diverge.
This commit is contained in:
@@ -170,9 +170,11 @@ def _slot_priority(topic: str, posts: List[Dict[str, Any]]) -> List[Dict[str, An
|
||||
posts that rerank later demotes as entity misses starves the on-topic
|
||||
posts the user actually sees (2026-06-06 "OpenClaw vs Hermes" run:
|
||||
2,000+ upvote Gemma/GPU threads took every slot, then were demoted to
|
||||
zero). Mirror rerank's demotion signal — the topic's stripped primary
|
||||
entity contained in the post text — so slots go to posts likely to
|
||||
survive final ranking. Falls back to token-overlap relevance when the
|
||||
zero). Mirror rerank's demotion signal via the shared `_entity_grounded`
|
||||
check (head token of the topic's stripped primary entity present in the
|
||||
post text) so slots go to posts likely to survive final ranking — keying
|
||||
on the same head token keeps the two paths from diverging. Falls back to
|
||||
token-overlap relevance when the
|
||||
topic yields no usable primary entity. Within each tier the incoming
|
||||
(score-first) order is preserved. Never raises; on any failure the
|
||||
incoming order is returned unchanged.
|
||||
@@ -186,7 +188,7 @@ def _slot_priority(topic: str, posts: List[Dict[str, Any]]) -> List[Dict[str, An
|
||||
entity = rerank._primary_entity(topic).lower()
|
||||
if entity:
|
||||
def _matches(post: Dict[str, Any]) -> bool:
|
||||
return entity in _post_text(post).lower()
|
||||
return rerank._entity_grounded(_post_text(post).lower(), entity)
|
||||
else:
|
||||
prepared = relevance.PreparedQuery(topic)
|
||||
|
||||
|
||||
@@ -247,6 +247,29 @@ def _candidate_haystack(candidate: schema.Candidate) -> str:
|
||||
return " ".join(parts).lower()
|
||||
|
||||
|
||||
def _entity_grounded(haystack: str, primary_entity: str) -> bool:
|
||||
"""True if the candidate text plausibly mentions the primary entity.
|
||||
|
||||
Grounds on the HEAD token of the primary entity (the brand / proper-noun
|
||||
core), not the full multi-word phrase. Trailing tokens are usually category
|
||||
descriptors the user/planner appended for search ("Stripe payments"), not
|
||||
part of the entity, so requiring the whole phrase over-demotes on-entity
|
||||
items that omit the descriptor. Items that never name the brand at all still
|
||||
miss the head token and stay demoted.
|
||||
|
||||
Trade-off: a proper noun with a generic head ("New York Times" -> "new")
|
||||
under-demotes rather than over-demotes - the safe direction, since the
|
||||
observed harm was burying real high-engagement signal. Substring (not
|
||||
word-boundary) matching is likewise deliberate: it catches plurals and
|
||||
compounds ("stripes"), and vacuous matches from very short heads ("X",
|
||||
"Go") merely disable the penalty rather than burying good items.
|
||||
"""
|
||||
tokens = primary_entity.lower().split()
|
||||
if not tokens:
|
||||
return True
|
||||
return tokens[0] in haystack
|
||||
|
||||
|
||||
def _fallback_tuple(candidate: schema.Candidate, *, primary_entity: str = "") -> tuple[float, str]:
|
||||
score = (
|
||||
(candidate.local_relevance * 100.0 * 0.7)
|
||||
@@ -254,17 +277,15 @@ def _fallback_tuple(candidate: schema.Candidate, *, primary_entity: str = "") ->
|
||||
+ (candidate.source_quality * 100.0 * 0.1)
|
||||
)
|
||||
reason = "fallback-local-score"
|
||||
# Entity-grounding demotion: if the primary entity (topic minus intent
|
||||
# modifier) is not present anywhere in the candidate's text surfaces
|
||||
# Entity-grounding demotion: subtract ENTITY_MISS_PENALTY when the candidate
|
||||
# never mentions the primary entity's head token, across all text surfaces
|
||||
# (title, snippet, transcript, transcript highlights, top comments,
|
||||
# insights), subtract ENTITY_MISS_PENALTY. Skip for candidates with
|
||||
# NO text anywhere (e.g., image-only TikToks) to avoid penalizing
|
||||
# thin-text sources unfairly. 2026-04-19 Nate Herk "Managed Agents"
|
||||
# video ranked #2 on a Hermes query despite zero Hermes mentions
|
||||
# because the old haystack only checked title + snippet.
|
||||
# insights). Skip for candidates with NO text anywhere (e.g. image-only
|
||||
# TikToks) so thin-text sources aren't penalized unfairly. See
|
||||
# _entity_grounded for why grounding keys on the head token, not the phrase.
|
||||
if primary_entity:
|
||||
haystack = _candidate_haystack(candidate)
|
||||
if haystack.strip() and primary_entity.lower() not in haystack:
|
||||
if haystack.strip() and not _entity_grounded(haystack, primary_entity):
|
||||
score -= ENTITY_MISS_PENALTY
|
||||
reason = "fallback-local-score (entity-miss demotion)"
|
||||
return max(0.0, min(100.0, score)), reason
|
||||
|
||||
Reference in New Issue
Block a user