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:
+23
-7
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user