feat(recall): dynamische Google-News-Volltext-Suchfeeds pro Lage

Recall-Problem: Die Pipeline durchsuchte nur ~28 feste site:-RSS-Feeds plus
Claude-WebSearch. Japanische Security-Vendor-Blogs, Fachportale und
Regionalmedien (Cybertrust, ITmedia, INTERNET Watch, Reuters Japan ...)
tauchten in keinem festen Feed auf. Bei der Test-Lage "Qilin Ransomware
Japan" fand die Pipeline 20 Kandidaten — eine generische Google-News-JP-
Suche zum selben Thema liefert 49.

Fix: researcher.build_news_search_feeds baut pro Refresh einen Google-News-
Volltext-Suchfeed je Sprache (news.google.com/rss/search?q=keywords&hl=..&gl=..).
Query = Top-4-Keywords der jeweiligen Sprache aus der Keyword-Extraktion.
Der Orchestrator haengt diese Feeds an die selektierten site:-Feeds an; sie
laufen durch dieselbe Pipeline (Keyword-Match, Pre-Topic-Translate,
Topic-Filter). Precision bleibt, Recall steigt.

- researcher.py: build_news_search_feeds + _GNEWS_LOCALE-Tabelle.
- orchestrator._rss_pipeline: Suchfeeds aus source_language_whitelist
  (jp_demo: ['ja']) bzw. output+research_language (normale Orgs) gebaut
  und an selected_feeds angehaengt.
- rss_parser._apply_domain_cap: Suchfeeds (domain 'google-news-search-<lang>')
  bekommen Cap 25 statt 10 — sie sind der Recall-Treiber, Topic-Filter
  uebernimmt die Precision.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
2026-05-22 01:02:47 +02:00
Ursprung 16d1133442
Commit 0e4c78d50a
3 geänderte Dateien mit 113 neuen und 4 gelöschten Zeilen

Datei anzeigen

@@ -6,6 +6,11 @@ import httpx
from datetime import datetime, timezone
from config import TIMEZONE, MAX_ARTICLES_PER_DOMAIN_RSS
from source_rules import _extract_domain
# Cap fuer dynamische Google-News-Suchfeeds — hoeher als der normale Domain-Cap,
# weil ein Suchfeed gezielt fuer breiten Recall gebaut wird. Topic-Filter
# entscheidet danach ueber die Precision.
MAX_ARTICLES_PER_DOMAIN_RSS_SEARCH = 25
from feeds.transcript_extractors._common import html_to_text
from services.post_refresh_qc import normalize_german_umlauts
from agents.researcher import keywords_for_language, flatten_keywords
@@ -276,10 +281,15 @@ class RSSParser:
for domain, domain_articles in by_domain.items():
# Nach Relevanz sortieren (beste zuerst)
domain_articles.sort(key=lambda a: a.get("relevance_score", 0), reverse=True)
kept = domain_articles[:MAX_ARTICLES_PER_DOMAIN_RSS]
if len(domain_articles) > MAX_ARTICLES_PER_DOMAIN_RSS:
# Dynamische Google-News-Suchfeeds ("google-news-search-<lang>") sind
# der Recall-Treiber und bekommen einen hoeheren Cap als feste Feeds.
cap = (MAX_ARTICLES_PER_DOMAIN_RSS_SEARCH
if domain.startswith("google-news-search-")
else MAX_ARTICLES_PER_DOMAIN_RSS)
kept = domain_articles[:cap]
if len(domain_articles) > cap:
logger.info(
f"Domain-Cap: {domain} von {len(domain_articles)} auf {MAX_ARTICLES_PER_DOMAIN_RSS} Artikel begrenzt"
f"Domain-Cap: {domain} von {len(domain_articles)} auf {cap} Artikel begrenzt"
)
capped.extend(kept)