Zurück zur Startseite
Leistungen

Backend Engineering, DevOps

Branche

B2B-Marktplatz

Jahr

2022-2024

Suchagenten, die wissen, wann sie schweigen.

Ein Nutzer eines B2B-Marktplatzes hatte eine breite Suche gespeichert und bekam 30 E-Mails am Tag. Ein anderer hatte eine enge gespeichert, hörte wochenlang nichts und bekam dann einen Schwall veralteter Ergebnisse. Beide gingen. 4.000 Nutzer hatten Suchen wie diese gespeichert, und der Posteingang war der einzige Weg der Plattform, einen von ihnen zurückzuholen.

DIE HERAUSFORDERUNG

Jeder Nutzer bekommt einen anderen Posteingang

4.000 aktive Nutzer, ein bis fünf gespeicherte Agenten pro Person, vier Frequenzen: stündlich, täglich, wöchentlich, monatlich. Die erste Implementierung schickte jeden Treffer sofort raus. Unter Last hieß das: bei jedem Lauf die gesamte Inseratsdatenbank scannen, jedes Inserat gegen jeden Agenten prüfen und pro Treffer eine E-Mail. Ein Inserat, das zum täglichen und zum wöchentlichen Agenten eines Nutzers passte, ging zweimal raus. SendGrid begann, die Schübe zu drosseln. Nutzer mit breiten Filtern stellten den Absender stumm, Nutzer mit engen vergaßen, dass es die Plattform gab. Nichts hielt fest, was ein Agent schon gezeigt hatte, also konnte nichts verhindern, dass er es noch einmal zeigte.

DIE LÖSUNG

Celery Beat mit gestaffelter Ratenbegrenzung

Eine eigene Messaging-Plattform war der erste Vorschlag, verworfen, weil die Matching-Logik und jede Regel darüber, wer was bekommen darf, schon im Django-ORM lebten. Ein Cronjob war der zweite, verworfen, weil er einen eigenen Prozess, eine eigene Datenbankverbindung, Fehlerbehandlung und Logging gebraucht hätte. Stattdessen ist jeder Agent eine ScheduledMailReport-Zeile mit Kriterien und Frequenz des Nutzers. Celery Beat startet vier Tasks, einen pro Stufe. Ein Task lädt nur Inserate, die seit dem letzten Lauf des Agenten entstanden sind, grenzt die Empfänger mit Exists-Subqueries ein (bestätigte Adresse, nicht als Spam gemeldet, nicht gesperrt, aktiv) und sammelt Treffer. Drei Limits stapeln sich: max_mails_per_run begrenzt E-Mails pro Ausführung, max_companies begrenzt Ergebnisse pro E-Mail, und eine Many-to-Many-Tabelle, companies_reported, hält jedes (Agent, Inserat)-Paar fest, das rausgegangen ist. Der Preis ist diese Tabelle: Sie wächst mit jedem Versand und braucht einen Index und einen Aufräumjob. Dafür gibt sie eine Garantie, die kein Zeitstempel geben kann.

Die Versandschleife. Das exclude liest die Tabelle, das add schreibt sie nach dem Versand:

Python
def send_scheduled_reports(frequency):
    agents = ScheduledMailReport.objects.filter(
        frequency=frequency,
        is_active=True,
        user__in=eligible_users,
    ).select_related('user')

    sent_count = 0
    for agent in agents:
        if sent_count >= settings.MAX_MAILS_PER_RUN:
            break

        new_companies = (
            Company.objects
            .filter(**agent.get_criteria())
            .filter(created_at__gte=agent.last_run)
            .exclude(
                pk__in=agent.companies_reported.all()
            )[:settings.MAX_COMPANIES_PER_MAIL]
        )

        if new_companies.exists():
            send_report_email(agent.user, new_companies)
            agent.companies_reported.add(*new_companies)
            agent.last_run = timezone.now()
            agent.save(update_fields=['last_run'])
            sent_count += 1
            time.sleep(0.1)  # SendGrid throttle

Drei Agenten, ein Nutzer

Oben kommen Inserate mit einem Branchen-Tag an. Drei Agenten desselben Nutzers feuern in verdichteten Intervallen: stündlich nur Logistik, täglich Logistik und Maschinenbau, wöchentlich alles. Achte auf die Tabelle companies_reported: Schickt der tägliche Agent ein Maschinenbau-Inserat, wird das Paar aus derselben E-Mail auch für den wöchentlichen Agenten eingetragen, und der nächste Wochenlauf zählt es als bereits gemeldet.

Agentenläufe
Neue Inserate
  • #1052Logistik
  • #1051Maschinenbau
  • #1050Chemie
  • #1049Logistik
  • #1048Maschinenbau
  • #1047Logistik
  • #1046Chemie
  1. StündlichLogistik9 Läufe

    1 gefunden0 bereits gemeldet1 gesendet

  2. TäglichLogistik, Maschinenbau3 Läufe

    3 gefunden2 bereits gemeldet1 gesendet

  3. Wöchentlichalle Branchen1 Läufe

    10 gefunden7 bereits gemeldet3 gesendet

companies_reported
Inserateingetragen fürE-Mail
#1051TäglichWöchentlich
#1052StündlichTäglichWöchentlich
#1050Wöchentlich
#1046Wöchentlich
#1043Wöchentlich
#1049StündlichTäglichWöchentlich
#1048TäglichWöchentlich
#1047StündlichTäglichWöchentlich
Inserate erstellt
12
E-Mails gesendet
9
Ergebnisse zugestellt
12
Duplikate vermieden
12
Paare eingetragen
26

DAS ERGEBNIS

Von Rauschen zu Signal

Die Nutzer öffneten die E-Mails wieder: Die Öffnungsrate hat sich nach der Umstellung mehr als verdreifacht, und die Beschwerden über Benachrichtigungs-Spam hörten auf. Alle 4.000 Nutzer über vier Stufen sind in unter 90 Sekunden verarbeitet, auf Celery Beat, Django-ORM-Queries und einer einzigen M2M-Tabelle. Eine Messaging-Plattform wurde nicht gekauft, und das Produktteam ändert eine Frequenz im Schedule statt per Deployment.

WICHTIGE KENNZAHLEN

41%E-Mail-Öffnungsrate nach der Umstellung, vorher 12 %
4.000+Nutzer mit aktiven Suchagenten
23%Als Duplikat verworfene Ergebnisse, Ø pro Lauf

FÜR DEIN PROJEKT

  • Wann es passt

    Dein Produkt mailt Leuten neue Einträge, die zu gespeicherten Kriterien passen: Inserate, Jobs, Alerts, Preissenkungen. Wenn ein Eintrag zu zwei Abos derselben Person passen kann, hast du dieses Problem bereits.

  • Was du prüfen solltest

    Schau nach, was dein System nach einem Versand festhält. Wenn die Antwort ein Zeitstempel und sonst nichts ist, kannst du nicht belegen, dass ein Nutzer einen Eintrag genau einmal gesehen hat.

  • Was es braucht

    Ein Scheduler, den du ohnehin betreibst (hier Celery Beat), eine Join-Tabelle mit zusammengesetztem Index und ein monatlicher Aufräumjob. Keine Messaging-Plattform, kein zusätzlicher Dienst.

KUNDENSTIMME

Früher sind unsere Nutzer entweder in E-Mails ertrunken oder haben vergessen, dass es uns gibt. Jetzt schickt ein Agent, wenn er etwas Neues hat, und schweigt sonst. Support-Tickets zu Benachrichtigungen gingen von zehn pro Woche auf null.

Product Manager

B2B-Marktplatz

FAQ

TECHNOLOGIE-STACK

Django
Python
PostgreSQL

Daniel hat bei der Gründung und Umsetzung der Unternehmen Solvster und VIARES einen entscheidenden Beitrag geleistet. Er war verantwortlich für den gesamten technischen Aufbau der Onlinepräsenz, inklusive der Serversetups, der Entwicklung und Optimierung der Website sowie der Sicherstellung der Funktionalität. Besonders beeindruckt hat mich, wie proaktiv und lösungsorientiert er immer an Verbesserungen gearbeitet hat. Daniel war stets sehr responsiv und zuverlässig - es war eine Freude, mit ihm zusammenzuarbeiten. Seine Expertise und sein Engagement haben maßgeblich zum Erfolg beider Projekte beigetragen!

Dietmar Eglhofer - Geschäftsführer, Solvster & VIARES

Schließe dich Dietmar an und starte dein nächstes Projekt mit Zuversicht.

Schreibe mirSchreibe mir