perf: optimize dedup, parallelize handle searches and enrichment

The dedup hot path recomputed normalize_text() 4 times per comparison
and recomputed item_text() on every inner-loop iteration. Pre-computing
n-gram sets and token sets into a _PreparedText cache cuts dedup time
by 6x (2.16s to 0.39s on 300 unique items).

Bird handle searches spawned one Node process per handle sequentially.
Now uses ThreadPoolExecutor so N handles run concurrently. Same pattern
applied to YouTube comment enrichment (was serial, Reddit was already
parallel) and the retry-thin-sources phase in the pipeline.

Clustering now pre-computes candidate text and uses prepared_similarity
for the O(n^2) grouping and MMR representative selection loops.

Minor: _is_wsl() cached with lru_cache, Bundle.add_items() uses
extend() instead of list concatenation.

End-to-end: 5.2s -> 3.7s (29% faster) on a typical 4-source query.
This commit is contained in:
Ilia Alshanetsky
2026-04-09 19:00:30 -04:00
parent 252c8222f1
commit eef3547c37
7 changed files with 128 additions and 59 deletions
+23 -7
View File
@@ -57,28 +57,34 @@ def _entity_overlap(entities_a: set[str], entities_b: set[str]) -> float:
def _mmr_representatives(
candidates: list[schema.Candidate],
text_cache: dict[str, dedupe._PreparedText],
limit: int = 3,
diversity_lambda: float = 0.75,
) -> list[str]:
selected: list[schema.Candidate] = []
remaining_set = {c.candidate_id for c in candidates}
remaining = list(candidates)
while remaining and len(selected) < limit:
if not selected:
best = max(remaining, key=lambda candidate: candidate.final_score)
selected.append(best)
remaining.remove(best)
remaining_set.discard(best.candidate_id)
remaining = [c for c in remaining if c.candidate_id in remaining_set]
continue
selected_preps = [text_cache[c.candidate_id] for c in selected]
def score(candidate: schema.Candidate) -> float:
prep = text_cache[candidate.candidate_id]
diversity_penalty = max(
dedupe.hybrid_similarity(_candidate_text(candidate), _candidate_text(existing))
for existing in selected
dedupe.prepared_similarity(prep, sp) for sp in selected_preps
)
return (diversity_lambda * candidate.final_score) - ((1 - diversity_lambda) * diversity_penalty * 100)
best = max(remaining, key=score)
selected.append(best)
remaining.remove(best)
remaining_set.discard(best.candidate_id)
remaining = [c for c in remaining if c.candidate_id in remaining_set]
return [candidate.candidate_id for candidate in selected]
@@ -105,15 +111,21 @@ def cluster_candidates(
)
return clusters
text_cache: dict[str, dedupe._PreparedText] = {
c.candidate_id: dedupe._PreparedText(_candidate_text(c))
for c in candidates
}
groups: list[list[schema.Candidate]] = []
# Lower threshold for breaking_news: related articles share fewer exact
# words but cover the same event.
threshold = 0.42 if plan.intent == "breaking_news" else 0.48
for candidate in candidates:
assigned = False
cand_prep = text_cache[candidate.candidate_id]
for group in groups:
leader = group[0]
similarity = dedupe.hybrid_similarity(_candidate_text(candidate), _candidate_text(leader))
similarity = dedupe.prepared_similarity(cand_prep, text_cache[leader.candidate_id])
if similarity >= threshold:
group.append(candidate)
assigned = True
@@ -125,7 +137,7 @@ def cluster_candidates(
for index, group in enumerate(groups, start=1):
group.sort(key=lambda candidate: candidate.final_score, reverse=True)
cluster_id = f"cluster-{index}"
representatives = _mmr_representatives(group)
representatives = _mmr_representatives(group, text_cache)
for candidate in group:
candidate.cluster_id = cluster_id
clusters.append(
@@ -225,7 +237,11 @@ def _merge_entity_clusters(
# Pick representatives from combined pool
combined_candidates = [candidate_map[cid] for cid in combined_cids if cid in candidate_map]
combined_candidates.sort(key=lambda c: c.final_score, reverse=True)
reps = _mmr_representatives(combined_candidates)
merge_text_cache = {
c.candidate_id: dedupe._PreparedText(_candidate_text(c))
for c in combined_candidates
}
reps = _mmr_representatives(combined_candidates, merge_text_cache)
cluster_id = cl.cluster_id
for cid in combined_cids: