Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions home-mixer/candidate_hydrators/filtered_topics_hydrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,24 @@ fn decode_topics_pair(
match decoded {
StratoResult::Ok(v) => {
let ft = v.v;
let exp_topics = ft
.as_ref()
.and_then(|ft| ft.topic_ids_for_experiment(experiment).cloned());
// Some([]) = lookup succeeded with no topics. None is
// reserved for fetch/decode failure so Explore can fail closed.
let exp_topics = Some(
ft.as_ref()
.and_then(|ft| ft.topic_ids_for_experiment(experiment).cloned())
.unwrap_or_default(),
);
let unf_topics = if need_unfiltered {
ft.as_ref().and_then(|ft| {
ft.topic_ids_for_experiment(TopicFilteringExperiment::Unfiltered)
.cloned()
})
Some(
ft.as_ref()
.and_then(|ft| {
ft.topic_ids_for_experiment(
TopicFilteringExperiment::Unfiltered,
)
.cloned()
})
.unwrap_or_default(),
)
} else {
None
};
Expand Down
60 changes: 57 additions & 3 deletions home-mixer/filters/topic_ids_filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,20 @@ impl Filter<ScoredPostsQuery, PostCandidate> for TopicIdsFilter {
let all_topic_ids = TopicIdExpansion::all_production_topic_ids();
let excluded: HashSet<i64> = all_topic_ids.difference(&expanded).copied().collect();

// Bulk Explore is an exclusion filter: keep posts whose known
// topics are not all outside the requested set. Missing topic
// hydration (None) must fail closed — otherwise withheld
// Sports/Politics/etc. posts still rank when Strato misses.
// Some([]) is a successful lookup with no topics (untagged).
let (kept, removed): (Vec<_>, Vec<_>) =
candidates
.into_iter()
.partition(|c| match &c.filtered_topic_ids {
Some(candidate_topics) if !candidate_topics.is_empty() => {
!candidate_topics.iter().all(|tid| excluded.contains(tid))
}
_ => true,
Some(_) => true,
None => false,
});
(kept, removed)
} else {
Expand Down Expand Up @@ -881,8 +887,56 @@ mod tests {
let result = TopicIdsFilter.filter(&query, candidates);
let kept_ids: Vec<u64> = result.kept.iter().map(|c| c.tweet_id).collect();
let removed_ids: Vec<u64> = result.removed.iter().map(|c| c.tweet_id).collect();
assert_eq!(kept_ids, vec![1, 3, 4, 6, 7]);
assert_eq!(removed_ids, vec![2, 5]);
assert_eq!(kept_ids, vec![1, 4, 6, 7]);
assert_eq!(removed_ids, vec![2, 3, 5]);
}

#[test]
fn test_bulk_explore_drops_hydration_miss_not_known_untagged() {
let query = ScoredPostsQuery {
topic_ids: vec![
TopicIdExpansion::XAI_NEWS,
TopicIdExpansion::BUSINESS_FINANCE,
TopicIdExpansion::SCIENCE_TECHNOLOGY,
TopicIdExpansion::XAI_MOVIES_TV,
TopicIdExpansion::XAI_AI,
TopicIdExpansion::XAI_GAMING,
TopicIdExpansion::XAI_CRYPTOCURRENCY,
],
..Default::default()
};

let candidates = vec![
PostCandidate {
tweet_id: 1,
filtered_topic_ids: None,
..Default::default()
},
PostCandidate {
tweet_id: 2,
filtered_topic_ids: Some(vec![]),
..Default::default()
},
PostCandidate {
tweet_id: 3,
filtered_topic_ids: Some(vec![TopicIdExpansion::XAI_SPORTS_REAL]),
..Default::default()
},
];

let result = TopicIdsFilter.filter(&query, candidates);
let kept_ids: Vec<u64> = result.kept.iter().map(|c| c.tweet_id).collect();
let removed_ids: Vec<u64> = result.removed.iter().map(|c| c.tweet_id).collect();
assert_eq!(
kept_ids,
vec![2],
"known-empty topic list is untagged For You filler"
);
assert_eq!(
removed_ids,
vec![1, 3],
"hydration miss and withheld Sports must not rank on Explore"
);
}

#[test]
Expand Down