Cross-Platform
Postbox verknuepft Profile plattformuebergreifend (YouTube mit Instagram) und bietet plattformspezifische Related-Erkennung. Drei Systeme arbeiten zusammen:
- Cross-Platform Related Profiles: YouTube ↔ Instagram Verknuepfung
- Related Channels: YouTube-only (API-basiert)
- Related Profiles: Instagram-only (Keyword-basiert)
Gemeinsame Optimierungen (10-Punkte-Plan)
Alle drei Systeme teilen sich folgende Verbesserungen:
AI-Keywords Integration
Gemini AI-Keywords (ai_keywords JSONB-Feld) werden gleichgewichtet mit lokal extrahierten Keywords zusammengefuehrt:
$aiKeywords = $this->extractAiKeywords($profile);
// Normalisiert: lowercase, trimmed, min 2 Zeichen, max 10 Keywords
$allKeywords = array_unique(array_merge($localKeywords, $aiKeywords));
Location: app/Services/Related/CrossPlatformRelatedCalculator.php, app/Jobs/Instagram/FindRelatedInstagramProfiles.php
Parsed Links + Manuelle Links Scoring
Links aus social_profile_links werden batch-geladen und im Scoring beruecksichtigt:
| Link-Typ | Punkte | Beschreibung |
|---|---|---|
| Approved (curated) | 30 | Admin-genehmigte Links aus Kontaktdaten |
| Auto-parsed (unapproved) | 25 | Automatisch geparste Links aus Beschreibungen |
| Generic @handle | 20 | Handle-Referenzen in Bio (nur Instagram) |
N+1-Fix: Approved Links werden per batchLoadApprovedLinks() vor der Scoring-Schleife geladen.
Location: app/Services/Related/CrossPlatformRelatedCalculator.php
Logarithmische Follower-Similarity
Statt diskreter Tier-Zuordnung wird die log10-Differenz der Follower-Zahlen verwendet:
$diff = abs(log10($sourceFollowers) - log10($candidateFollowers));
// < 0.5: 10pt, < 1.0: 7pt, < 1.5: 4pt, < 2.0: 2pt, >= 2.0: 0pt
Beispiel: 10K vs 50K = diff 0.7 → 7 Punkte. 10K vs 1M = diff 2.0 → 0 Punkte.
Minimum Relevance Score
Alle drei Systeme nutzen MIN_RELEVANCE_SCORE = 15. Kandidaten unter diesem Schwellenwert werden verworfen.
Konfigurierbare Favorites-Tiers
Die Punkte-Zuordnung liegt in config/postbox.php:
'favorites_tiers' => [50 => 15, 20 => 12, 10 => 9, 5 => 6, 2 => 3, 1 => 1],
'favorites_tiers_instagram' => [20 => 10, 10 => 8, 5 => 6, 3 => 4, 1 => 2],
Batch-Update fuer Recalculate-Jobs
RecalculateRelatedInstagramScores und RecalculateRelatedChannelScores nutzen eine einzige SQL-Query mit CASE/WHEN statt N einzelner UPDATEs:
UPDATE instagram_related_profiles
SET relevance_score = (CASE WHEN id = ? THEN ? WHEN id = ? THEN ? END)::smallint,
updated_at = ?
WHERE id IN (?, ?)
Location: app/Jobs/Instagram/RecalculateRelatedInstagramScores.php, app/Jobs/YouTube/RecalculateRelatedChannelScores.php
Dynamische Stopwords
Der woechentliche keywords:update-stopwords Command analysiert Keyword-Haeufigkeit und cached uebermaessig verbreitete Keywords (>30% der Profile) als Stopwords in Redis (8 Tage TTL). Der ProfileKeywordExtractor prueft dynamische Stopwords vor der Keyword-Speicherung.
php artisan keywords:update-stopwords --dry-run
php artisan keywords:update-stopwords --threshold=25 # Custom Schwelle
Schedule: Sonntag 02:00 UTC
Location: app/Console/Commands/UpdateKeywordStopwords.php, app/Services/Instagram/ProfileKeywordExtractor.php
Cross-Platform Related Profiles
Datenmodell
// Model: CrossPlatformRelatedProfile
// Tabelle: cross_platform_related_profiles
$fillable = [
'source_profile_id', // YouTube ODER Instagram Profil
'related_profile_id', // Die andere Plattform
'relevance_score', // 0-100
'match_reason', // Erklaerung des Matchings
'cross_profile_verified_at', // Admin-Verifizierung (Timestamp)
'cross_profile_verified_by', // Admin-User-ID
];
Admin-Verifizierung
Admins koennen Cross-Platform-Zuordnungen ueber den AiFieldEditor verifizieren. Verifizierte Verknuepfungen:
- Werden im Gemini-Prompt als zusaetzlicher Kontext verwendet (Partner-Handle + Follower-Zahl)
- Ermoeglichen Kategorie-/Keyword-Propagation zwischen Partnern
- Loesen Mismatch-Alerts aus, wenn die AI-Kategorien divergieren
Scope: verified() — WHERE cross_profile_verified_at IS NOT NULL
Location: app/Models/CrossPlatformRelatedProfile.php
URL-Detection via ProfileDescriptionParser
Der Parser analysiert Profil-Beschreibungen und extrahiert Links zur jeweils anderen Plattform:
- YouTube-Beschreibungen werden nach Instagram-Links durchsucht
- Instagram-Bios werden nach YouTube-Links durchsucht
- Erkannte Links werden in der
social_profile_links-Tabelle gespeichert (ersetztparsed_linksJSONB)
Location: app/Services/Social/ProfileDescriptionParser.php
Matching Criteria (Score 0-100)
Der Relevance Score setzt sich aus mehreren Faktoren zusammen:
| Faktor | Gewicht | Beschreibung |
|---|---|---|
| Keywords | 30 | AI-Keywords + lokal extrahierte Keywords |
| Category | 25 | Gleiche AI-Kategorie |
| Parsed Links | 25-30 | Approved (30pt), auto-parsed (25pt), generic (20pt) |
| Favorites | 15 | Konfigurierbar via config('postbox.favorites_tiers') |
| Follower-Similarity | 10 | Logarithmische Aehnlichkeit (log10-Differenz) |
| Name | 10 | Namens-Aehnlichkeit (similar_text()) |
| Language | 10 | Gleiche erkannte Sprache |
Minimum-Schwelle: MIN_RELEVANCE_SCORE = 15
PostgreSQL Full-Text-Search
YouTube-Kandidatensuche nutzt tsvector + GIN-Index statt LIKE-Queries:
-- Migration: Generated stored column
ALTER TABLE social_profiles ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (to_tsvector('simple', COALESCE(title, '') || ' ' || COALESCE(description, ''))) STORED;
CREATE INDEX idx_social_profiles_search_vector ON social_profiles USING GIN (search_vector);
-- Query
WHERE search_vector @@ to_tsquery('simple', 'keyword1 | keyword2')
Location: database/migrations/2026_02_20_100001_add_fts_index_to_social_profiles.php
Schedule
# Taeglich 06:00 UTC
php artisan cross-platform:queue-related
Der Command dispatcht FindCrossPlatformRelatedProfiles Jobs fuer Profile, die:
- Tracking aktiviert haben
- Nicht excluded sind (blocked, sanitized, archived)
- Noch keine Cross-Platform-Relations haben oder deren Relations veraltet sind
Location: app/Jobs/CrossPlatform/FindCrossPlatformRelatedProfiles.php
UI-Komponente
Watchers\CrossPlatformRelated
--> Zeigt verknuepfte Profile der anderen Plattform
--> Horizontaler Slider mit Profil-Karten
--> "Ansehen" / "Hinzufuegen" Buttons
Location: app/Livewire/Watchers/CrossPlatformRelated.php, resources/views/livewire/watchers/cross-platform-related.blade.php
Related Channels (YouTube-only)
API-basierte Suche nach aehnlichen YouTube-Kanaelen.
Datenmodell
| Tabelle | Beschreibung |
|---|---|
youtube_related_channels | Relationen (social_profile_id → related_social_profile_id + relevance_score) |
pending_youtube_channel_imports | Channels, die wegen Quota-Erschoepfung noch importiert werden muessen |
Status-Tracking auf social_profiles:
related_channels_status:pending,running,completed,pending_imports,failedrelated_channels_searched_at: Timestamp der letzten Sucherelated_channels_error: Fehlermeldung
Location: app/Models/YouTubeRelatedChannel.php, app/Models/PendingYouTubeChannelImport.php
Ablauf
graph TD
A["User/Auto-Fill"] --> B["FindRelatedYouTubeChannels Job"]
B --> C{"AI-Daten vorhanden?"}
C -->|Nein, Attempt 1| D["DetectProfileLanguage dispatchen"]
D --> E["Job release(300) — 5min Delay"]
E --> B
C -->|Ja / Attempt >= 2| F["Suchstrategien 1-8 ausfuehren"]
F --> G["YouTube Search API"]
G --> H["Relevance Scoring"]
H --> I{"Score >= 15?"}
I -->|Ja| J["Top 25 speichern"]
I -->|Nein| K["Verworfen"]
J --> L["Unbekannte Kanaele importieren"]
L --> M["RelatedProfilesCalculated Event"]
- User (oder Auto-Fill-Command) loest Suche aus
- AI-Enhancer Prerequisite: Falls kein
detected_atvorhanden, wirdDetectProfileLanguagedispatcht und der Job mit 5min Delay released. Bei Attempt >= 2 wird ohne AI-Daten fortgefahren. FindRelatedYouTubeChannelsJob sucht via YouTube Data API (Search + Topic IDs + AI Keywords)- Relevance Score wird berechnet (min. 15 Punkte)
- Neue Kanaele werden automatisch in Postbox importiert (via
EnsureSocialProfileFromUrl) - Top 25 Ergebnisse werden als Related Channels gespeichert
Suchstrategien
| Prioritaet | Strategie | Beschreibung |
|---|---|---|
| 1 | Title Keywords + Primary Topic | Spezifischste Suche |
| 2 | Title Keywords only | Breitere Suche |
| 3 | First Word + Topic | Fallback |
| 4 | First Word only | Noch breiter |
| 5 | Additional Topics | Weitere Themen |
| 6 | Description Keywords | Bio-basiert |
| 7 | AI Keywords | Gemini-generierte semantische Keywords |
| 8 | Title + Country | Lokalisiert |
Relevance Scoring (0-100)
| Faktor | Max Punkte | Beschreibung |
|---|---|---|
| Exists in Postbox | 35 | Kann sofort angezeigt werden |
| Subscriber Bonus | 0-20 | Mehr Abonnenten = relevanter |
| Favorites Bonus | 0-10 | Haeufig favorisiert in Postbox |
| Position Bonus | 1-20 | Fruehere Suchergebnisse bevorzugt |
| Topic Match | 10 | Passendes YouTube-Topic |
| Title Similarity | 0-5 | Aehnlicher Kanalname |
| "- Topic" De-Rank | -50% | Auto-generierte YouTube-Sammelkanaele werden halbiert |
"- Topic" De-Rank: YouTube erstellt automatisch Aggregate-Kanaele wie "Gaming - Topic" oder "Music - Topic". Diese enthalten keine eigenen Inhalte und haben keinen Discovery-Wert. Kanaele deren Titel mit - Topic endet, erhalten eine 50%-Reduktion auf den berechneten Score. Der De-Rank wird nach allen Bonus-Berechnungen und vor dem min(100, $score) Cap angewendet.
Minimum-Schwelle: MIN_RELEVANCE_SCORE = 15
Berechnungsbeispiele
Beispiel 1: Normaler Kanal (100K Abonnenten, Postbox-Match)
| Faktor | Punkte |
|---|---|
| Exists in Postbox | +35 |
| Subscriber Bonus (100K) | +11 |
| Position 3 | +18 |
| Topic Match | +10 |
| Title Similarity (45%) | +3 |
| Gesamt | = 77 |
Beispiel 2: "- Topic" Kanal (gleiche Bedingungen)
| Faktor | Punkte |
|---|---|
| Exists in Postbox | +35 |
| Subscriber Bonus (100K) | +11 |
| Position 3 | +18 |
| Topic Match | +10 |
| Title Similarity (45%) | +3 |
| Zwischensumme | = 77 |
| "- Topic" De-Rank (×0.5) | = 39 |
Beispiel 3: Kleiner "- Topic" Kanal ohne Postbox-Match
| Faktor | Punkte |
|---|---|
| Exists in Postbox | +0 |
| Subscriber Bonus (5K) | +5 |
| Position 10 | +11 |
| Topic Match | +10 |
| Title Similarity (20%) | +0 |
| Zwischensumme | = 26 |
| "- Topic" De-Rank (×0.5) | = 13 |
| Ergebnis | Verworfen (< 15) |
Location: FindRelatedYouTubeChannels::calculateRelevanceScores(), RecalculateRelatedChannelScores::calculateScore()
Auto-Fill Command
# Alle 5 Minuten (nur bei >25% Research-Quota verfuegbar)
php artisan youtube:auto-fill-related-channels
php artisan youtube:auto-fill-related-channels --dry-run
php artisan youtube:auto-fill-related-channels --force
Prioritaet 1: Profile ohne Related Channels (nach Followers sortiert) Prioritaet 2: Profile mit veralteten Relations (>30 Tage, aelteste zuerst)
Refresh-Limits
- Regular Users: Maximal einmal alle 30 Tage
- Admins: Jederzeit
Location: app/Livewire/Watchers/RelatedChannels.php, app/Jobs/YouTube/FindRelatedYouTubeChannels.php, app/Console/Commands/AutoFillRelatedYouTubeChannels.php
Queue
Related Channels Jobs laufen auf einer dedizierten Queue:
php artisan queue:work database --queue=youtube-related-channels --sleep=5 --timeout=300 --tries=2
Related Profiles (Instagram-only)
Keyword-basierte Suche nach aehnlichen Instagram-Profilen aus der lokalen Datenbank (keine API-Calls).
Datenmodell
| Tabelle | Beschreibung |
|---|---|
instagram_related_profiles | Relationen mit relevance_score und match_reason |
instagram_profile_keywords | Extrahierte Keywords aus Bio, Titel, Handle |
Status-Tracking auf social_profiles:
related_profiles_status:pending,running,completed,failedrelated_profiles_calculated_at: Timestamprelated_profiles_error: Fehlermeldung
Location: app/Models/InstagramRelatedProfile.php, app/Models/InstagramProfileKeyword.php
Keyword-Extraktion
ProfileKeywordExtractor extrahiert Keywords aus:
- Bio-Text (Split by Whitespace/Punctuation)
- Titel/Display-Name
- Handle (Split by Underscores/Dots)
Stopword-Removal: Statische Listen (DE/EN/ES/FR/IT) + dynamische Stopwords aus Redis-Cache (keywords:update-stopwords). Minimum 3 Zeichen pro Keyword.
Location: app/Services/Instagram/ProfileKeywordExtractor.php
Source-gewichtete Keyword-Scoring
Keywords werden nach Herkunft unterschiedlich gewichtet:
| Source | Punkte pro Match | Begruendung |
|---|---|---|
bio | 6 | Bio-Inhalt ist am aussagekraeftigsten |
title | 5 | Display-Name zeigt Thema |
handle | 3 | Handle-Woerter sind weniger spezifisch |
| Default | 4 | Fallback fuer unbekannte Sources |
Relevance Scoring (max 100)
| Faktor | Max Punkte | Berechnung |
|---|---|---|
| Parsed Link Match | 30 | Approved Link (30pt), auto-parsed (25pt), generic @handle (20pt) |
| Popularity Bonus | 20 | Absolute Follower-Zahl (1M+ = 20 Punkte) |
| Bio-Keyword-Match | variabel | Source-gewichtet (bio=6, title=5, handle=3 pro Match) |
| Follower-Aehnlichkeit | 15 | log10-Skala-Vergleich |
| Favorites-Bonus | 10 | Konfigurierbar via config('postbox.favorites_tiers_instagram') |
| Name-Aehnlichkeit | 10 | similar_text() auf Profil-Titel |
| Gleicher Profile-Typ | 10 | Business/Creator/Private Match |
| Sprache/Land | 10 | +5 gleiche Sprache, +5 gleiches Land |
Minimum-Schwelle: MIN_RELEVANCE_SCORE = 15
Trigger
Related Profiles werden automatisch nach dem ersten erfolgreichen Scrape berechnet:
Instagram Scrape erfolgreich
--> InstagramDailyScrapeProcessor extrahiert Keywords
--> AI-Keywords aus ai_keywords JSONB werden hinzugemerged
--> FindRelatedInstagramProfiles Job wird dispatcht
--> Min. 2 Keyword-Matches erforderlich
--> Nur Kandidaten >= 15 Punkte werden gespeichert
--> Top 25 als Related Profiles gespeichert
Location: app/Jobs/Instagram/FindRelatedInstagramProfiles.php, app/Livewire/Watchers/RelatedProfiles.php
Ausschluss-Filter
Alle Related-Queries muessen excluded Profile filtern:
->whereHas('relatedProfile', fn ($q) => $q
->whereNull('blocked_at')
->whereNull('sanitized_at')
->whereNull('archived_at')
)