Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8.0 KiB
Phase 13: Scraping Adapters & Cross-Source Deduplication - Context
Gathered: 2026-07-23 Status: Ready for planning Mode: mvp (vertical slices)
## Phase BoundaryErweitert die Ausschreibungs-Abdeckung um zwei zusätzliche Quellen neben DÖE (Phase 10): AI-AG-NetServer-Portale (lhs-vpbw, tender24, vergabe.landbw) über EINEN config-getriebenen Adapter, und den cosinex-Vergabemarktplatz (DTVP) über einen separaten Adapter. Dieselbe Ausschreibung, die über mehrere Quellen auftaucht, wird zu einem Trefferlisten-Eintrag mit Links zu allen Quell-Portalen zusammengefasst (Fuzzy-Fingerprint-Dedup). Das System verweigert strukturell (im Code, nicht nur dokumentiert) das Registrieren der AGB-verbotenen Portale vergabe24 und aumass.
Requirements: INGEST-02, INGEST-03, INGEST-07, SCHEMA-03.
Nicht in dieser Phase: RSS/E-Mail-Quellen (Phase 14), Admin-Quellen-UI + ausgeschlossene-Portale-Anzeige UI-06 (Phase 14), i18n-Rollout (Phase 14). Keine neuen Benachrichtigungs-/Filter-Features (10-12 fertig).
## Implementation DecisionsBestätigt durch User-Abfrage 2026-07-23:
Scraping-Tiefe
- D-01: Robuster Kern zuerst, Scraping best-effort. Das config-getriebene Adapter-Framework, der Fuzzy-Dedup und die harte Denylist werden voll gebaut + getestet. Die konkreten Scraping-Selektoren/HTTP-Details werden so weit gefüllt, wie das jeweilige Portal es zulässt; blockende/instabile Portale werden dokumentiert (nicht erzwungen). Ziel: nachrüstbarer, getesteter Kern — kein fragiles All-or-Nothing-Scraping.
- D-02: Machbarkeit der realen Portale (NetServer lhs-vpbw/tender24/vergabe.landbw, cosinex/DTVP) ist ein Research-Punkt (wie DÖE in Phase 10): Live prüfen, ob HTML-Scraping oder ein Export/Feed-Endpunkt existiert, Rate-Limits, robots/AGB. Ergebnis steuert, wie weit die Selektoren gefüllt werden.
Cross-Source-Dedup (SCHEMA-03)
- D-03: Ein Eintrag + Liste ALLER Quell-Links. Eine über mehrere Quellen deduplizierte Ausschreibung ist EIN
Tender, der alle Quell-Portale + deren Links/NoticeIds führt (neue Kind-TabelleTenderSource, 1:n). Die Trefferliste zeigt einen Eintrag; die Detailansicht listet alle Quellen mit Links. NICHT „Primärquelle gewinnt, Rest verworfen". - D-04: Dedup-Schlüssel-Hierarchie (SCHEMA-03): OCID →
Quelle:NoticeId→ Fuzzy-Fingerprint aus Auftraggeber + Titel + CPV + Frist + Wert. Beim Ingest einer neuen Quelle: existiert OCID/NoticeId-Match → gleiche Ausschreibung; sonst Fingerprint gegen Bestand prüfen; Match → als zusätzlicheTenderSourceanhängen statt neuenTenderanzulegen. - D-05: Dedup greift erst ab der zweiten aktiven Quelle. Mit nur DÖE aktiv läuft keine Fuzzy-Dedup-Logik (Erfolgskriterium 3). Backfill der bestehenden ~2000+ DÖE-Tender: jeder bekommt eine
TenderSource-Zeile aus seinen aktuellensourcePortal/sourceNoticeId/sourceUrl.
Denylist (INGEST-07)
- D-06: vergabe24 und aumass sind im Adapter-/Quellen-Registry als harte Denylist hinterlegt — der Versuch, sie als automatische Poll-/Scraping-Quelle zu registrieren, wird im Code abgelehnt (Exception/Fehler), nicht nur per Doku. Test beweist die Ablehnung. Grund: AGB-Verbot automatisierter Zugriffe; Oberschwellen-Daten kommen ohnehin über DÖE.
Claude's Discretion
- Fingerprint-Algorithmus: Normalisierung (lowercase, Umlaute, Whitespace, CPV-Divisions-Präfix, Wert-Bucketing, Frist-Datum) + Hash oder Ähnlichkeits-Schwellwert; genaue Felder-Gewichtung. Muss NULL-tolerant sein (Frist/Wert oft null — Phase-11-Befund).
- Adapter-Framework: Verallgemeinerung von
tender-source-adapter.interface.ts(Phase 10) + Registry mit Denylist-Gate; wieTenderIngestionService.pollDueSources/TenderSourcePollConfigauf mehreresourceTypeerweitert wird (poll-once-fan-out-many bleibt). - Scraping-Client pro Portal (native
fetch, evtl. HTML-Parser), Pagination/Rate-Limit, Fehlertoleranz. - Datenmodell
TenderSource: Felder, Unique-Constraints (z.B.@@unique([sourcePortal, sourceNoticeId])), Beziehung zuTender, wiededupKey/primäresourcePortal-Felder migriert werden.
<canonical_refs>
Canonical References
Downstream agents MUST read these before planning or implementing.
Phasen-Vorgaben
.planning/ROADMAP.md§ Phase 13 — Ziel + 4 Erfolgskriterien. Mode: mvp..planning/REQUIREMENTS.md— INGEST-02, INGEST-03, INGEST-07, SCHEMA-03 (+ INGEST-04/UI-06 sind Phase 14, NICHT hier).
Phase-10-Fundament (Adapter + Ingestion — verbindlich)
apps/api/src/tenders/tender-source-adapter.interface.ts— bestehendeTenderSourceAdapter-Abstraktion; hier verallgemeinern + Registry.apps/api/src/tenders/adapters/doe-opendata.adapter.ts(+ .spec) — Referenz-Adapter (Fetch → Normalize), Muster für NetServer/cosinex.apps/api/src/tenders/tender-ingestion.service.ts(pollDueSources) +tender-scheduler.service.ts— poll-once-fan-out-many-Scheduler; auf mehrere Quellen erweitern (NICHT DKV-findFirst).apps/api/src/tenders/tender-normalizer.service.ts— Normalisierung (buyer/cpv/region/bundesland/value); Fingerprint baut darauf auf.apps/api/prisma/schema.prisma→model Tender(sourcePortal, sourceNoticeId, ocid,dedupKey @unique, contentHash, cpvCodes[], buyerName, deadlineAt, estimatedValue) undTenderSourcePollConfig(sourceType).
Phase-11-Fundament (Anzeige der Multi-Source-Links)
apps/web/.../modules/tender-radar/(ResultsList, TenderDetail) — Detailansicht muss dieTenderSource-Liste (mehrere Quell-Links) anzeigen;tenders.controller.tsRead-Endpunkte liefern sie mit.
Milestone-Research
.planning/research/ARCHITECTURE.md—TenderSourceAdapter-Abstraktion, Dedup-Strategie (OCID→Quelle:NoticeId→Fuzzy), global-vs-tenant-Split..planning/research/PITFALLS.md— Multi-Tenant-Scheduler, Scraping-Fallen, Dedup-Kollisionen..planning/research/ausschreibungs-portale-feasibility.md— bekannte Portal-Infos (DÖE + Ausblick NetServer/cosinex). </canonical_refs>
<code_context>
Existing Code Insights
Reusable Assets
tender-source-adapter.interface.ts+doe-opendata.adapter.ts— Adapter-Vertrag + Referenz.tender-ingestion.service.ts/tender-scheduler.service.ts— Ingestion-Pipeline + Scheduler (poll-once-fan-out-many).tender-normalizer.service.ts— Feld-Normalisierung; Grundlage fürs Fingerprinting.tender-query.builder.ts/ Phase-11-Results-UI — Trefferliste/Detail; erweitern um Multi-Source-Anzeige.- Native
fetchals HTTP-Client (kein axios); Prisma-Migrationen handgeschrieben, lokal viadocker compose exec -T db psqlanwenden (kein Docker-Deploy Testserver).
Established Patterns
dedupKey @uniqueist aktuell der SCHEMA-02-Upsert-Target (ocid | sourcePortal:sourceNoticeId). Für SCHEMA-03 kommt die Quell-Ebene inTenderSource; der kanonischeTenderbleibt der eine Eintrag.- Hardcodiertes Deutsch (i18n = Phase 14). Per-user/tenant-Scoping nur wo nötig — Tender bleibt plattform-global (D-03 Phase 10).
Integration Points
- Neue Tabelle
TenderSource(1:n zu Tender) + Backfill der Bestands-Tender. - Adapter-Registry mit Denylist-Gate (vergabe24/aumass).
- NetServer-Adapter (config-getrieben, mehrere Portale) + cosinex/DTVP-Adapter.
- Fingerprint-Dedup im Ingestion-Upsert-Pfad (nur ab 2. aktiver Quelle).
- Detailansicht/Results zeigen alle Quell-Links. </code_context>
- Erfolgskriterium 3 + 4 sind die harten Prüfsteine: (3) ein Tender aus DÖE+Scraper erscheint EINMAL mit allen Quell-Links; (4) vergabe24/aumass-Registrierung wird vom Code abgelehnt (Test).
- „Robuster Kern, Scraping nachrüstbar" (D-01): Framework + Dedup + Denylist müssen auch dann getestet grün sein, wenn ein Portal live gerade nicht scrapebar ist — die Adapter-Grenze kapselt das.
- Fingerprint muss NULL-tolerant sein (Frist/Wert oft leer — 91,6% ohne Wert, viele ohne Frist; Phase-11-Live-Befund), sonst kollabiert Dedup oder matcht falsch.
- Konsistenz mit Phase 10: poll-once-fan-out-many, kein findFirst; Tender global.