Back to "Zero PII Kundenintelligenz Teil 2:® Profile Signale Segmente"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

C# Privacy Product Segmentation

Zero PII Kundenintelligenz Teil 2:® Profile Signale Segmente

Wednesday, 31 December 2025

Im Teil wir deckten die Philosophie und die Serie Übersicht Teil Wir bauten die Probendatengenerator .. .

Jetzt lassen Sie let-' s bauen das Kernsystem. Dieses Teil konzentriert sich auf...

  1. Zero---PII-Profilarchitektur Ephemerale Sitzungen gegen persistente Profile
  2. Signale und Gewichte Was wir verfolgen und wie es sich ansammelt
  3. Segmentdefinitionen Fuzzy-Mitgliedschaft mit gewichteten Regeln Das Hauptereignis
  4. Voransicht des Outbox-Musters Verlässliche Veröffentlichung von Ereignissen

Das ist die Beispielprojekt (Mostlylucid.SegmentCommerceEine kleine E-Commerce-Demo, die das Kernmuster zeigt, ., es wird absichtlich vereinfacht, um Konzepte klar zu demonstrieren ,, aber es enthält auch reale Infrastruktur-Muster (outbox .- based Message Handling . , Hintergrund Job Processing . ,, JSONB Indexierung .), dass Sie ' skalieren horizontal in der Produktion .

Betrachten Sie dies als "Produktionsmuster in einem einzigen -app-Formfaktor. Die gleiche Codestruktur funktioniert, ob Sie einen Prozess ausführen oder über Dienstleistungen verteilen möchten.

Technologiewahl

Wir wählten eine Einfacher, zusammenhängender Stack um die Probe klar zu halten, während sie Produktionsmuster zeigt

PostgreSQL Eine Datenbank

Anstelle von separaten Vektordatenbanken verwenden wir Nachrichtenqueues und Caching Layers. PostgreSQL:

flowchart LR
    App[ASP.NET Core] --> PG[(PostgreSQL)]
    
    PG --> JSONB[JSONB<br/>interests]
    PG --> Vector[pgvector<br/>embeddings]
    PG --> Queue[Queue tables<br/>jobs, outbox]
    PG --> FTS[Full-text<br/>tsvector]
    
    style PG stroke:#2f9e44,stroke-width:3px
    style JSONB stroke:#1971c2,stroke-width:2px
    style Vector stroke:#1971c2,stroke-width:2px
    style Queue stroke:#1971c2,stroke-width:2px
    style FTS stroke:#1971c2,stroke-width:2px
  • JSONB für flexible Schemata
  • pgvector für die Einbettung von (,no Qdrant-/,Pinecone-),
  • LEBENSMITTEL für Jobs .( .no Redis . / .RabbitMQ .) .
  • tsvector für Volltext---Text - Elastische Suche - )-

Eine Verbindung Pool-Verbindungspool-Verbindung 1 Backup-Verbindung

HTMX-Datenverarbeitungsgeräte für Alpine-., :, Server, UI

<!-- Instant search (no page reload) -->
<input 
    hx-get="/api/search" 
    hx-trigger="keyup changed delay:300ms" 
    hx-target="#results" />
  • HTMXServer gibt Teilwerte wieder ,( ,no JSON ,→ ,templatierend ,)
  • Alpen-.-jsReaktivität ohne Bauschritt
  • Fortschrittliche VerbesserungFunktioniert ohne JS.,, besser mit ihm

SPA---ähnliche UX mit Server---Rendering Einfachheit

ASP-.-NET-Kern ,:-Verteilbare Muster

Einzel-App, aber Musterskala, :

  • Auslauf Auswechseln für Nachrichtenbus
  • Job-Warteschlange ](-LISTEN-/-NOTIFY-)-Swap für Arbeitnehmerpool
  • Sitzungssammler Swap für separate API

Einfach starten - .-Verteilen, wenn erforderlich -.-

Was wir vermieden haben

  • Keine separate Vektordatenbank ist kolodiert.
  • Kein JS-Framework (-HTMX +-Alpine ist einfacher
  • Noch keine Mikroservices arbeiten in Monolith oder verteilt
  • Keine Docker-Kompositausbreitung

Die Zero---PII-Herausforderung

Traditionelles Benutzer-Tracking speichert identifizierbare Informationen.:-Namen.,-E-Mails.MS K2-Benutzer-IDs, die mit dem Verhalten verbunden sind..-DSGVO und Datenschutzbestimmungen machen diese zunehmend problematischer.

Wir speichern Verhaltensmuster... nicht Identitäten...

flowchart LR
    subgraph "Traditional Approach"
        User1[User: [email protected]] --> Behavior1[Bought headphones]
        Behavior1 --> PII1[(PII Database)]
    end
    
    subgraph "Zero-PII Approach"
        User2[Anonymous Session] --> Behavior2[Bought headphones]
        Behavior2 --> Pattern[(Category: tech<br/>Signal weight: 1.0)]
    end
    
    style PII1 stroke:#c92a2a,stroke-width:3px
    style Pattern stroke:#2f9e44,stroke-width:3px

Die wichtigsten Erkenntnisse du musst nicht wissen, wer jemand ist zu wissen, WAS sie interessiert?.

Architektur im Überblick

Das System hat drei Kernschichten.

flowchart TB
    Browser[Browser] --> Session[Session Collector]
    Session --> Profile[Persistent Profile]
    Profile --> Segments[Segment Service]
    
    Session -->|in-memory only| Cache[(IMemoryCache)]
    Profile -->|elevated signals| DB[(PostgreSQL + JSONB)]
    Segments -->|memberships| UI[Segment Explorer UI]
    
    style Session stroke:#1971c2,stroke-width:3px
    style Profile stroke:#2f9e44,stroke-width:3px
    style Segments stroke:#fab005,stroke-width:3px
    style Cache stroke:#e64980,stroke-width:2px
  1. Sitzungssammler --Erfasst Verhaltenssignale (-Ansichten,-Klicks,-Warenkorb fügt hinzu)---Aufnahmen Nur in Erinnerung
  2. Persistentes Profil -, erhöht hohe Wertesignale ,, berechnet Interessen Datenbank ,- , unterstützt
  3. Segmentdienst Bewertet Profile gegen RegelnBewertet Mitgliedschaften

Kritische UnterscheidungDie Sitzungen sind ephemer, .. Profile sind persistent,.. Dies ist kein Implementierungs-Detail.-.it' ist eine Entscheidung für die Privatsphäre-Architektur.

Sitzungsprofile Strict In--Memory-Cache von (LFU)

Sitzungen sind ausschließlich in der --SpeicherSie leben in IMemoryCache mit gleitendem Ablauf und nie die Datenbank berühren.

Dies ist ein harter architektonischer Constraint.:-Session-Daten können nicht persistent werden..-Es sammelt Signale während eines Besuchs und elikt nach Minuten der Inaktivität über LFU.MSC3-Least Häufig verwendete Cache-Richtlinien unter Speicherdruck.

Sitzungsprofil - Das In-Memory-Modell

// Mostlylucid.SegmentCommerce/Models/SessionProfile.cs
public class SessionProfile
{
    public string SessionKey { get; set; } = string.Empty;

    // Category interest scores: { "tech": 0.75, "fashion": 0.25 }
    public Dictionary<string, double> Interests { get; set; } = new();

    // Detailed signal counts: { "tech": { "product_view": 5, "add_to_cart": 1 } }
    public Dictionary<string, Dictionary<string, int>> Signals { get; set; } = new();

    // Products viewed this session
    public List<int> ViewedProducts { get; set; } = new();

    // Session context (device, referrer domain, time-of-day)
    public SessionContext? Context { get; set; }

    // Aggregates
    public double TotalWeight { get; set; }
    public int SignalCount { get; set; }
    public int PageViews { get; set; }
    public int ProductViews { get; set; }
    public int CartAdds { get; set; }

    // Timestamps
    public DateTime StartedAt { get; set; } = DateTime.UtcNow;
    public DateTime LastActivityAt { get; set; } = DateTime.UtcNow;

    // Link to persistent profile (if fingerprint resolved)
    public Guid? PersistentProfileId { get; set; }
}

Gespeichert in IMemoryCache oder IDistributedCache mit Sliding Expiration

// Mostlylucid.SegmentCommerce/Services/Profiles/SessionCollector.cs
_cache.Set(sessionKey, sessionProfile, new MemoryCacheEntryOptions
{
    SlidingExpiration = TimeSpan.FromMinutes(30),
    Priority = CacheItemPriority.Normal, // LFU eviction under memory pressure
    
    // CRITICAL: Eviction callback decides whether to elevate to persistent profile
    PostEvictionCallbacks =
    {
        new PostEvictionCallbackRegistration
        {
            EvictionCallback = async (key, value, reason, state) =>
            {
                if (value is SessionProfile session && ShouldElevate(session))
                {
                    // Only NOW do we write to database (persistent profile)
                    await ElevateToProfileAsync(session);
                }
                // Otherwise: session is gone forever
            }
        }
    }
});

Starke Einschränkungen

  • Keine versehentliche PersistenzLive-Sitzungen Nur im Cache -(-IMemoryCache oder IDistributedCache wie Redis-)-
  • LFU-VertreibungUnter dem Druck des Gedächtnisses zuerst die Sitzungen evakuieren
  • Ablauf des GleitvorgangsMinuten vor der letzten Aktivität
  • RäumungsaufrufLetzte Chance, hohe - Wertesignale zu erhöhen bevor sie verloren gehen
  • Keine WiedereinziehungDie App startet alle Sitzungen neu, es sei denn, sie verwendet IDistributedCache, ,, aber immer noch ephemeral.

Elevationsentscheidung über die Räumung

private bool ShouldElevate(SessionProfile session)
{
    // Elevate if:
    // - User added to cart (high intent)
    // - User completed purchase (conversion)
    // - Fingerprint was resolved (identity established)
    // - Session weight exceeds threshold (engaged visitor)
    
    return session.CartAdds > 0 
        || session.TotalWeight > 5.0 
        || session.PersistentProfileId.HasValue;
}

Warum die Räumung zurückruft

Das ist die Nur Sicherer Punkt, um Persistenz zu entscheiden.

  1. Wir kennen den gesamten Sitzungsverlauf
  2. Wir können das Gesamtengagement bewerten
  3. Wir vermeiden die Speicherung von Low---Wert-Sitzungen
  4. Wir garantieren, dass die Sitzungen nicht versehentlich fortbestehen

Wenn wir nicht während der Räumung erhöhen, ist die Sitzung fort für immerDies ist das Design.

SitzungKontext, was wir verfolgen

public class SessionContext
{
    public string? DeviceType { get; set; }        // "mobile", "desktop"
    public string? EntryPath { get; set; }         // "/products/tech" (no query params)
    public string? ReferrerDomain { get; set; }    // "google.com" (domain only, not full URL)
    public string? TimeOfDay { get; set; }         // "morning", "afternoon"
    public string? DayType { get; set; }           // "weekday", "weekend"
}

Beachten Sie was, ' nicht Hier werden die IP-Adressen der , user agents, volle URLs, Messpixel der, tracking erfasst KontextmusterNicht identifizierbare Informationen

Was sind Signale?

A Signal ist ein Verhalten Tatsache während einer Benutzer-Action erfasst, statt Tracking-"hessen, die ,"h wir verfolgen, was passierte, ."h

Dieses Konzept kommt von ephemere Signale- Lived Fakten, die in einem begrenzten Fenster existieren und aus natürlichem Alter .. . In diesem System ., Operationen senden Signale wie "api.rate_limited" oder "gateway.slow" Verhalten ohne enge Kopplung zu koordinieren

Hier wenden wir das gleiche Muster auf das Benutzerverhalten an.

  • Produktansicht → product_view Signal Gewicht :
  • In den Warenkorb legen → add_to_cart Signal Gewicht :
  • Kauf → purchase Signal Gewicht :

Signale sind Vergänglich Auslöschen mit der Sitzung Null---PII Nichtzutreffendes streichen. gewichtet (-Intent--Bewusst

Kunden-Fingerabdruck-Methoden zur Identifizierung von Cookies

Um Sitzungen zu verlinken, ohne Cookies zu verfolgen, verwenden wir client--'side FingerabdruckDer Browser berechnet einen Hash aus Signalen , ( timezone ,, , Bildschirmauflösung , , , WebGL Renderer, , , Canvas-Fingerabdruck , und sendet nur der Hash zu /api/fingerprint.

Der Server dann HMACs, die Hasch mit einem geheimen Schlüssel, der es an anderer Stelle unbrauchbar macht

Dieser Code ist von meinem Bot-Detektionsprojekt,, wo es verwendet wird, um Schaber zu identifizieren; ., gleiche Technik; ,, verschiedene Zwecke; MSC4,

// Mostlylucid.SegmentCommerce/ClientFingerprint/fingerprint.js
// Collect signals (browser capabilities, not PII)
var signals = [
    Intl.DateTimeFormat().resolvedOptions().timeZone,
    navigator.language,
    screen.width + 'x' + screen.height,
    // ... (see full code)
];

// Hash locally
var hash = hash(signals.join('|'));

// Send only the hash via sendBeacon
navigator.sendBeacon('/api/fingerprint', JSON.stringify({ h: hash }));

Server---Seite:-

// Server HMACs the client hash with a secret key
var profileKey = HMACSHA256(clientHash + secretKey);

Jetzt haben wir einen stabilen usersite-Bezeichner ohne Cookies oder lokaleStorage. Vollständige Fingerabdruckquelle Angepasst aus meistluzider ., Botdetektion, ).

Signaltypen und Gewichte

Verschiedene Aktionen haben unterschiedliche Intent-Levels. Grundgewichte.

SignalTypen: Die Gewichtshierarchie

// Mostlylucid.SegmentCommerce/Data/Entities/Profiles/SignalEntity.cs
public static class SignalTypes
{
    // Passive signals (low intent)
    public const string PageView = "page_view";              // 0.01
    public const string CategoryBrowse = "category_browse";  // 0.03
    public const string ProductImpression = "product_impression"; // 0.02

    // Active signals (medium intent)
    public const string ProductView = "product_view";        // 0.10
    public const string ProductClick = "product_click";      // 0.08
    public const string Search = "search";                   // 0.05

    // High-intent signals
    public const string AddToCart = "add_to_cart";           // 0.35
    public const string AddToWishlist = "add_to_wishlist";   // 0.25
    public const string ViewCart = "view_cart";              // 0.15
    public const string BeginCheckout = "begin_checkout";    // 0.40

    // Conversion signals (highest intent)
    public const string Purchase = "purchase";               // 1.00
    public const string Review = "review";                   // 0.60
    public const string Share = "share";                     // 0.50

    public static readonly Dictionary<string, double> BaseWeights = new()
    {
        { PageView, 0.01 },
        { ProductView, 0.10 },
        { AddToCart, 0.35 },
        { Purchase, 1.00 },
        // ... (see full code for complete list)
    };

    public static double GetBaseWeight(string signalType)
    {
        return BaseWeights.GetValueOrDefault(signalType, 0.05);
    }
}

Warum das so wichtig ist

  • Eine einzelne Seitenansicht0.01Das Signal wird beherrscht
  • In den Warenkorb legen0.35Ein starkes Intent-Signal
  • Kauf von (1.00Das Signal ist das stärkste.

Diese Hierarchie verhindert, dass das Profil verschmutzt wird.

Sitzungssammler: Recording-Signale ( Editieren- Nur )

// Mostlylucid.SegmentCommerce/Services/Profiles/SessionCollector.cs
public async Task<SessionProfile> RecordSignalAsync(
    SessionSignalInput input, CancellationToken ct = default)
{
    var sessionKey = input.SessionKey;
    
    // Get or create session FROM CACHE (never DB)
    var session = _cache.Get<SessionProfile>(sessionKey);
    
    if (session == null)
    {
        session = new SessionProfile
        {
            SessionKey = sessionKey,
            StartedAt = DateTime.UtcNow
        };
    }

    session.LastActivityAt = DateTime.UtcNow;

    var weight = input.Weight ?? SignalTypes.GetBaseWeight(input.SignalType);

    // Update in-memory aggregates
    session.TotalWeight += weight;
    session.SignalCount++;

    if (!string.IsNullOrEmpty(input.Category))
    {
        session.Interests.TryGetValue(input.Category, out var currentScore);
        session.Interests[input.Category] = currentScore + weight;
    }

    if (input.SignalType == SignalTypes.AddToCart)
    {
        session.CartAdds++;
    }

    // Put back in cache with sliding expiration
    _cache.Set(sessionKey, session, new MemoryCacheEntryOptions
    {
        SlidingExpiration = TimeSpan.FromMinutes(30),
        Priority = CacheItemPriority.Normal,
        PostEvictionCallbacks = { /* elevation callback */ }
    });

    return session;
}

Schnell, weil

  • Rein in: -, Memory, keine DB schreibt:),
  • Kein Serialisierungsüberkopf
  • Keine Netzwerk-Aufrufe Lokaler Cache

Persistente Profile, erhöhte Signale

Wenn eine Sitzung zeigt, hohe Intent-(-cart fügt hinzu: ,, Käufe, ),, wir erhöhen Signale zu einem Persistentes Profil.

PersistentProfileEntity-ProfileDas lange Profil von -

// Mostlylucid.SegmentCommerce/Data/Entities/Profiles/PersistentProfileEntity.cs
[Table("persistent_profiles")]
public class PersistentProfileEntity
{
    [Key]
    public Guid Id { get; set; } = Guid.NewGuid();

    [Required]
    [MaxLength(256)]
    public string ProfileKey { get; set; } = string.Empty;

    // How this profile is identified (Fingerprint, Cookie, Identity)
    public ProfileIdentificationMode IdentificationMode { get; set; }

    // Behavioral data (all JSONB)
    [Column("interests", TypeName = "jsonb")]
    public Dictionary<string, double> Interests { get; set; } = new();

    [Column("affinities", TypeName = "jsonb")]
    public Dictionary<string, double> Affinities { get; set; } = new();

    [Column("brand_affinities", TypeName = "jsonb")]
    public Dictionary<string, double> BrandAffinities { get; set; } = new();

    [Column("price_preferences", TypeName = "jsonb")]
    public PricePreferences? PricePreferences { get; set; }

    [Column("traits", TypeName = "jsonb")]
    public Dictionary<string, bool> Traits { get; set; } = new();

    // Computed segments
    public ProfileSegments Segments { get; set; } = ProfileSegments.None;

    [Column("llm_segments", TypeName = "jsonb")]
    public Dictionary<string, double>? LlmSegments { get; set; }

    // Vector embedding for similarity matching
    [Column("embedding", TypeName = "vector(384)")]
    public Vector? Embedding { get; set; }

    // Statistics
    public int TotalSessions { get; set; }
    public int TotalSignals { get; set; }
    public int TotalPurchases { get; set; }
    public int TotalCartAdds { get; set; }

    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    public DateTime LastSeenAt { get; set; } = DateTime.UtcNow;
    public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

Immer noch Null PII:

  • ProfileKey ist ein HMAC-Hash, nicht reversibel
  • IdentificationMode erklärt uns, wie es identifiziert wurde.
  • Alle Daten sind VerhaltenssignaleNicht personenbezogene Daten

Erhöhte Anzahl der Sitzungen (MSK)

public async Task ElevateToProfileAsync(
    SessionProfileEntity session, 
    PersistentProfileEntity profile, 
    CancellationToken ct = default)
{
    if (session.IsElevated)
        return;

    // Merge interests (use higher value)
    foreach (var (category, score) in session.Interests)
    {
        if (!profile.Interests.ContainsKey(category) || 
            profile.Interests[category] < score)
        {
            profile.Interests[category] = score;
        }
    }

    // Update stats
    profile.TotalSessions++;
    profile.TotalSignals += session.SignalCount;
    profile.TotalCartAdds += session.CartAdds;
    profile.LastSeenAt = DateTime.UtcNow;
    profile.UpdatedAt = DateTime.UtcNow;

    // Mark session as elevated
    session.IsElevated = true;
    session.PersistentProfileId = profile.Id;

    // Clear segment cache (will be recomputed)
    profile.SegmentsComputedAt = null;
    profile.EmbeddingComputedAt = null;

    await _db.SaveChangesAsync(ct);
}

Wenn eine Erhöhung eintritt

  • Nach einem Warenkorb fügen Sie hinzu:
  • Nach einem Kauf
  • Wenn Benutzer sich für Persistenz entscheidet

Segmentdefinitionen – Die Ausschüttung

Die Segmente für zwei Teile sind die Durchführbare Ausgabe von allen, die Signalsammlung----sie antworten, was für eine Art Shopper ist diese-MSC2-

Warum Fuzzy Mitgliedschaft?

Traditionelle Segmentierung ist binär, :-you-'-yout entweder im Segment oder youou-'-iout nicht.-iOut Dies erzeugt Probleme

flowchart LR
    subgraph "Binary Segmentation"
        Profile1[3 purchases] -->|"Threshold: 5"| Out[NOT High-Value]
        Profile2[5 purchases] -->|"Threshold: 5"| In[High-Value]
    end

Ein Kunde mit Käufen von 4 wird identisch mit einem mit 0. behandelt, dass ' falsch ist.

Fuzzy Segmentierung gibt jedem Profil eine Punktzahl

Käufe Binärer |, Fuzzy Score |-----------|--------|-------------| Nicht verfügbar bei: Nicht verfügbar bei: Nicht verfügbar bei: ,| , 5+, |, Ja ,| , 1.0 , | ,

Jetzt können Sie proportional personalisieren ,: , , " , fast hoch , - , Wert , " , Kunden erhalten etwas andere Behandlung als ," , Nirgendwo in der Nähe

Die Segmentdefinitionsstruktur

// Mostlylucid.SegmentCommerce/Services/Segments/SegmentDefinition.cs
public class SegmentDefinition
{
    public string Id { get; set; }           // "tech-enthusiast"
    public string Name { get; set; }          // "Tech Enthusiasts"
    public string Description { get; set; }   // "Users with strong interest in technology"
    public string Icon { get; set; }          // "🔧"
    public string Color { get; set; }         // "#3b82f6"

    public List<SegmentRule> Rules { get; set; } = [];

    // How rules combine: All (AND), Any (OR), Weighted (sum)
    public RuleCombination Combination { get; set; } = RuleCombination.Weighted;

    // Minimum score to be "a member" (0-1)
    public double MembershipThreshold { get; set; } = 0.3;

    public List<string> Tags { get; set; } = [];  // For filtering/grouping
}

Regel-Typen, die Sie überprüfen können

Jede Regel wertet eine Dimension des Profils aus

public enum RuleType
{
    CategoryInterest,  // Check interests.tech, interests.fashion, etc.
    BrandAffinity,     // Check brandAffinities.Sony, brandAffinities.Nike, etc.
    PriceRange,        // Check price preferences (budget vs luxury)
    Trait,             // Check boolean traits (prefersDeals, browsesExtensively)
    Statistic,         // Check totalPurchases, totalSessions, totalCartAdds
    TagAffinity,       // Check affinities.gadgets, affinities.organic, etc.
    Recency,           // Check days since last activity
    Expression         // Custom expressions (advanced)
}

Regeloperatoren

public enum RuleOperator
{
    GreaterThan,       // value > threshold
    GreaterOrEqual,    // value >= threshold
    LessThan,          // value < threshold
    LessOrEqual,       // value <= threshold
    Equal,             // value == threshold
    NotEqual,          // value != threshold
    Contains,          // for array/string checks
    Between,           // for ranges
    In, NotIn          // for set membership
}

Regelkombinationsmethoden

Wie sich mehrere Regeln zu einem Endergebnis zusammensetzen

public enum RuleCombination
{
    All,      // AND logic: Score = min(all rule scores). All rules must pass.
    Any,      // OR logic: Score = max(all rule scores). Any rule can pass.
    Weighted  // Weighted sum: Score = Σ(rule.weight × rule.score) / Σ(rule.weight)
}

Gewichtet ist am häufigsten, lässt Sie sagen, " category interest matters 60%, recency mattings 30%, brand affinity matters

Reale Segmentbeispiele

Hier sind die Standard-Segmente im Beispielprojekt.

Tech-Enthusiasten

new SegmentDefinition
{
    Id = "tech-enthusiast",
    Name = "Tech Enthusiasts",
    Description = "Users with strong interest in technology products",
    Icon = "🔧",
    Color = "#3b82f6",
    MembershipThreshold = 0.35,
    Rules =
    [
        new() { 
            Type = RuleType.CategoryInterest, 
            Field = "interests.tech", 
            Operator = RuleOperator.GreaterOrEqual, 
            Value = 0.4, 
            Weight = 0.6,  // 60% of score
            Description = "Tech interest > 40%" 
        },
        new() { 
            Type = RuleType.TagAffinity, 
            Field = "affinities.gadgets", 
            Operator = RuleOperator.GreaterOrEqual, 
            Value = 0.2, 
            Weight = 0.2,  // 20% of score
            Description = "Likes gadgets" 
        },
        new() { 
            Type = RuleType.TagAffinity, 
            Field = "affinities.electronics", 
            Operator = RuleOperator.GreaterOrEqual, 
            Value = 0.2, 
            Weight = 0.2,  // 20% of score
            Description = "Likes electronics" 
        }
    ]
}

Beispiel für die Bewertung

  • Profil hat interests.tech = 0.72, affinities.gadgets = 0.31, affinities.electronics = 0.15
  • Regel 1:0.72>= -0.4- →Score 1.0- - (Überschreitungsschwelle
  • Regel 2:0.31>= -0.2- →Score 1.0- - (Überschreitungsschwelle
  • Regel-3: 0.15 <- - 0.2 , →-Score 0.75, (-partial-:
  • Schlussbemerkung 0.95
  • MSK-Schwellenwert Mitglied mit Vertrauen

Abtrünnige Wagen

new SegmentDefinition
{
    Id = "cart-abandoner",
    Name = "Cart Abandoners",
    Description = "Users who add items to cart but don't complete purchase",
    Icon = "🛒",
    Color = "#ef4444",
    MembershipThreshold = 0.4,
    Rules =
    [
        new() { 
            Type = RuleType.Statistic, 
            Field = "totalCartAdds", 
            Operator = RuleOperator.GreaterOrEqual, 
            Value = 3, 
            Weight = 0.5, 
            Description = "3+ cart adds" 
        },
        new() { 
            Type = RuleType.Statistic, 
            Field = "totalPurchases", 
            Operator = RuleOperator.LessThan, 
            Value = 2, 
            Weight = 0.5, 
            Description = "Few purchases" 
        }
    ]
}

Dieses fängt die -"...adds zum Warenkorb, aber tut es nicht kaufen -"... pattern -mesK3... perfekt für Wiederherstellungskampagnen...

Kunden mit hohem Wert

new SegmentDefinition
{
    Id = "high-value",
    Name = "High-Value Customers",
    Description = "Customers who make frequent purchases and spend above average",
    Icon = "💎",
    Color = "#8b5cf6",
    MembershipThreshold = 0.4,
    Rules =
    [
        new() { 
            Type = RuleType.Statistic, 
            Field = "totalPurchases", 
            Operator = RuleOperator.GreaterOrEqual, 
            Value = 3, 
            Weight = 0.4, 
            Description = "3+ purchases" 
        },
        new() { 
            Type = RuleType.PriceRange, 
            Field = "priceRange", 
            Value = "100-10000",  // High-end shoppers
            Weight = 0.3, 
            Description = "High price range" 
        },
        new() { 
            Type = RuleType.Recency, 
            Field = "lastSeen", 
            Operator = RuleOperator.LessThan, 
            Value = 30, 
            Weight = 0.3, 
            Description = "Active in last 30 days" 
        }
    ]
}

Jäger von Schnäppchen

new SegmentDefinition
{
    Id = "bargain-hunter",
    Name = "Bargain Hunters",
    Description = "Price-sensitive shoppers who love deals and discounts",
    Icon = "🏷️",
    Color = "#22c55e",
    MembershipThreshold = 0.3,
    Rules =
    [
        new() { 
            Type = RuleType.PriceRange, 
            Field = "priceRange", 
            Value = "0-75",  // Budget shoppers
            Weight = 0.5, 
            Description = "Low price preference" 
        },
        new() { 
            Type = RuleType.Trait, 
            Field = "traits.prefersDeals", 
            Value = true, 
            Weight = 0.3, 
            Description = "Prefers deals" 
        },
        new() { 
            Type = RuleType.Statistic, 
            Field = "totalCartAdds", 
            Operator = RuleOperator.GreaterThan, 
            Value = 5, 
            Weight = 0.2, 
            Description = "Shops around" 
        }
    ]
}

Alle Standardsegmente

Die Stichprobe umfasst Segmente, die allgemeine e-- commerce-Muster abdecken.

Abschnitt „|" Symbol Hauptregeln Anwendungsfall |---------|------|-----------|----------| Kunden mit hohem Wert , | , 💎 ,| , 3+ , Käufe von , , hohe Ausgaben , vor kurzem ,| , VIP-Behandlung , CRMK9 , Treueprogramme , | , Technologische Enthusiasten – | – MSC2 – MSV3 – MKK4 – Gadget-Affinität – MKS5 – Technische Produktempfehlungen – MSR6 – Mode nach vorn – | – 👗 – | – Mode-Interessen –, – mehrfache Besuche – | – Style-Empfehlungen – | – Verhandeln Jäger Vergünstigungen Vergünstigung Angebote Vergünstigungs-Meldungen Verbilligte Preisspanne Neue Besucher |, neue Besucher |, neue Mitglieder ≤2, keine Einkäufe |, Onboarding ,, erste Angebote -, Kauf Warenkorb Abtrünnige Waren , | , 🛒 ,| , 3+ , Warenkorb fügt hinzu ,, , wenige Einkäufe , E| , Wiederherstellung von E-Mails , Home-Enthusiasten Home-Begeisterte Home-Produkt cross--, verkaufen Sportliches Interesse an der Gesundheit von ,e Eigenschaften des Fitness-Produktfokus Loyal Customers Loyale Kunden – |, ⭐, |, MSC4, Käufe ,, 10+, Sitzungen – Retention – MSC8, Belohnungen |, Hochsignale , surft extensive Vergleichswerkzeuge , Detaillierte Informationen |

SegmentService®:-Computing Mitgliedschaften

Das SegmentService wertet Profile gegen alle Segmentregeln aus

// Mostlylucid.SegmentCommerce/Services/Segments/SegmentService.cs
public SegmentMembership EvaluateSegment(ProfileData profile, SegmentDefinition segment)
{
    var ruleScores = new List<RuleScore>();
    
    foreach (var rule in segment.Rules)
    {
        var (score, actualValue) = EvaluateRule(profile, rule);
        ruleScores.Add(new RuleScore
        {
            RuleDescription = rule.Description,
            Score = score,
            Weight = rule.Weight,
            ActualValue = actualValue  // For transparency
        });
    }

    // Combine based on segment's combination method
    double finalScore = segment.Combination switch
    {
        RuleCombination.All => ruleScores.Min(r => r.Score),
        RuleCombination.Any => ruleScores.Max(r => r.Score),
        RuleCombination.Weighted => ComputeWeightedScore(ruleScores),
        _ => 0
    };

    return new SegmentMembership
    {
        SegmentId = segment.Id,
        SegmentName = segment.Name,
        Score = Math.Round(finalScore, 3),
        IsMember = finalScore >= segment.MembershipThreshold,
        RuleScores = ruleScores,
        Confidence = score switch  // Human-readable
        {
            >= 0.8 => "Very High",
            >= 0.6 => "High",
            >= 0.4 => "Medium",
            >= 0.2 => "Low",
            _ => "Very Low"
        }
    };
}

Erklärbarkeit Built---In

Jedes Mitgliedsergebnis beinhaltet die tatsächliche Werte die zum Score geführt haben

// What the UI receives:
{
    "segmentId": "tech-enthusiast",
    "segmentName": "Tech Enthusiasts",
    "score": 0.95,
    "isMember": true,
    "confidence": "Very High",
    "ruleScores": [
        { "description": "Tech interest > 40%", "score": 1.0, "actualValue": "0.72" },
        { "description": "Likes gadgets", "score": 1.0, "actualValue": "0.31" },
        { "description": "Likes electronics", "score": 0.75, "actualValue": "0.15" }
    ]
}

Benutzer können sehen Sie genau, warum there-'-Re in einem Segment.Dies ist entscheidend für Transparenz und GDPR-Compliance.

Signalfluss (MSK)

Hier wird eine Produktansicht zu einer Segmentmitgliedschaft.

sequenceDiagram
    participant Browser
    participant Cache as SessionCache
    participant Outbox as Outbox
    participant Segment as SegmentService

    Browser->>Cache: Product view (category: "tech")
    Cache->>Cache: Update in-memory session
    Note over Cache: interests.tech += 0.10
    
    Browser->>Cache: Add to cart (high intent)
    Cache->>Outbox: Publish elevation event
    Outbox->>Outbox: Write to outbox table
    
    Note over Outbox: Background worker processes
    Outbox->>Segment: Elevate to PersistentProfile
    
    Segment->>Segment: Evaluate segment rules
    Note over Segment: interests.tech: 0.72 >= 0.40 ✓<br/>Score: 0.95, IsMember: true
    Segment-->>Browser: Segment memberships + explanations

Das Outbox-Muster

Alle wichtigen Aktionen fließen durch die outbox-MusterUnser primärer Orchestrierungsmechanismus

flowchart LR
    Action[Cart Add] --> TX[Single Transaction]
    TX --> DB[(Save + Outbox)]
    DB --> Worker[Background Worker]
    Worker --> Route[Route to Handlers]
    
    style TX stroke:#2f9e44,stroke-width:3px

Warum? Geschäftsdaten und Ereignisse werden in einer einzigen Transaktion geschrieben..-Events können verloren gehen!.-Fälle versuchen automatisch mit exponentiellem Backoff

// Every action publishes to outbox in the same transaction
await using var transaction = await _db.Database.BeginTransactionAsync(ct);

cart.Items.Add(new CartItem { ProductId = productId });
await _db.SaveChangesAsync(ct);

_outbox.Publish(OutboxEventTypes.ProductAddedToCart, new { ProductId = productId });
await _db.SaveChangesAsync(ct);

await transaction.CommitAsync(ct);
// Event is now guaranteed to be processed

Teil deckt die volle Outbox-Implementierung ab ,: , die Job Warteschlange , , , LISTEN , / , NOTIFY für sofortige Pickup , , , retry Logik und Skalierung Muster

Was kommt als nächstes?

Dieser Teil umfasste ,:'

  • Zero---PII-Profilarchitektur Vergängliche Sitzungen gegen persistente Profile
  • Segmentdefinitionen mit fuzzy Mitgliedschaft und gewichteten Regeln
  • Der SegmentService mit eingebauter ,-'in Erklärbarkeit

Teil geht tiefer ins :

  • Implementierung des Outbox-Musters Zuverlässiges Event Publishing und der einzelne Router
  • Job-Warteschlange mit PostgreSQL SKIP LOCKED für die verteilte Verarbeitung
  • LEBENSMITTEL für sofortigen Job Pickup
  • Benutzeroberfläche und Transparenz Das Armaturenbrett und der Segment-Explorer

Segmente sind die durchführbare Ausgabe dieses Systems..-Sie antworten "-Welche Art von Shopper ist das??"- mit fuzzy Scores.,-nicht binären Eimern..-Und jeder Benutzer kann genau sehen, warum sie sich in einem Segment befinden.

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.