Zum Hauptinhalt springen

Cross-Platform

Postbox verknuepft Profile plattformuebergreifend (YouTube mit Instagram) und bietet plattformspezifische Related-Erkennung. Drei Systeme arbeiten zusammen:

  1. Cross-Platform Related Profiles: YouTube ↔ Instagram Verknuepfung
  2. Related Channels: YouTube-only (API-basiert)
  3. 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

Links aus social_profile_links werden batch-geladen und im Scoring beruecksichtigt:

Link-TypPunkteBeschreibung
Approved (curated)30Admin-genehmigte Links aus Kontaktdaten
Auto-parsed (unapproved)25Automatisch geparste Links aus Beschreibungen
Generic @handle20Handle-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

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 (ersetzt parsed_links JSONB)

Location: app/Services/Social/ProfileDescriptionParser.php

Matching Criteria (Score 0-100)

Der Relevance Score setzt sich aus mehreren Faktoren zusammen:

FaktorGewichtBeschreibung
Keywords30AI-Keywords + lokal extrahierte Keywords
Category25Gleiche AI-Kategorie
Parsed Links25-30Approved (30pt), auto-parsed (25pt), generic (20pt)
Favorites15Konfigurierbar via config('postbox.favorites_tiers')
Follower-Similarity10Logarithmische Aehnlichkeit (log10-Differenz)
Name10Namens-Aehnlichkeit (similar_text())
Language10Gleiche erkannte Sprache

Minimum-Schwelle: MIN_RELEVANCE_SCORE = 15

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

API-basierte Suche nach aehnlichen YouTube-Kanaelen.

Datenmodell

TabelleBeschreibung
youtube_related_channelsRelationen (social_profile_idrelated_social_profile_id + relevance_score)
pending_youtube_channel_importsChannels, die wegen Quota-Erschoepfung noch importiert werden muessen

Status-Tracking auf social_profiles:

  • related_channels_status: pending, running, completed, pending_imports, failed
  • related_channels_searched_at: Timestamp der letzten Suche
  • related_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"]
  1. User (oder Auto-Fill-Command) loest Suche aus
  2. AI-Enhancer Prerequisite: Falls kein detected_at vorhanden, wird DetectProfileLanguage dispatcht und der Job mit 5min Delay released. Bei Attempt >= 2 wird ohne AI-Daten fortgefahren.
  3. FindRelatedYouTubeChannels Job sucht via YouTube Data API (Search + Topic IDs + AI Keywords)
  4. Relevance Score wird berechnet (min. 15 Punkte)
  5. Neue Kanaele werden automatisch in Postbox importiert (via EnsureSocialProfileFromUrl)
  6. Top 25 Ergebnisse werden als Related Channels gespeichert

Suchstrategien

PrioritaetStrategieBeschreibung
1Title Keywords + Primary TopicSpezifischste Suche
2Title Keywords onlyBreitere Suche
3First Word + TopicFallback
4First Word onlyNoch breiter
5Additional TopicsWeitere Themen
6Description KeywordsBio-basiert
7AI KeywordsGemini-generierte semantische Keywords
8Title + CountryLokalisiert

Relevance Scoring (0-100)

FaktorMax PunkteBeschreibung
Exists in Postbox35Kann sofort angezeigt werden
Subscriber Bonus0-20Mehr Abonnenten = relevanter
Favorites Bonus0-10Haeufig favorisiert in Postbox
Position Bonus1-20Fruehere Suchergebnisse bevorzugt
Topic Match10Passendes YouTube-Topic
Title Similarity0-5Aehnlicher 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)

FaktorPunkte
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)

FaktorPunkte
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

FaktorPunkte
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
ErgebnisVerworfen (< 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

Keyword-basierte Suche nach aehnlichen Instagram-Profilen aus der lokalen Datenbank (keine API-Calls).

Datenmodell

TabelleBeschreibung
instagram_related_profilesRelationen mit relevance_score und match_reason
instagram_profile_keywordsExtrahierte Keywords aus Bio, Titel, Handle

Status-Tracking auf social_profiles:

  • related_profiles_status: pending, running, completed, failed
  • related_profiles_calculated_at: Timestamp
  • related_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:

SourcePunkte pro MatchBegruendung
bio6Bio-Inhalt ist am aussagekraeftigsten
title5Display-Name zeigt Thema
handle3Handle-Woerter sind weniger spezifisch
Default4Fallback fuer unbekannte Sources

Relevance Scoring (max 100)

FaktorMax PunkteBerechnung
Parsed Link Match30Approved Link (30pt), auto-parsed (25pt), generic @handle (20pt)
Popularity Bonus20Absolute Follower-Zahl (1M+ = 20 Punkte)
Bio-Keyword-MatchvariabelSource-gewichtet (bio=6, title=5, handle=3 pro Match)
Follower-Aehnlichkeit15log10-Skala-Vergleich
Favorites-Bonus10Konfigurierbar via config('postbox.favorites_tiers_instagram')
Name-Aehnlichkeit10similar_text() auf Profil-Titel
Gleicher Profile-Typ10Business/Creator/Private Match
Sprache/Land10+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')
)