Zero PII Intelligence des clients - Partie 2: Profils, Signaux & Segments (Français (French))

Zero PII Intelligence des clients - Partie 2: Profils, Signaux & Segments

Wednesday, 31 December 2025

//

23 minute read

À l'intérieur Partie 1 Nous avons couvert la philosophie et l’aperçu de la série. Dans Partie 1.1 Nous avons construit le générateur de données d’échantillonnage.

Nous allons maintenant construire le système de base.

  1. Architecture du profil Zero-PII - Sessions éphémères vs profils persistants
  2. Signales et poids - Ce que nous repérons et comment il s'accumule
  3. Définitions de segments - Adhésion floue avec règles pondérées la manifestation principaleM SK2
  4. Vue préliminaire du modèle de la boîte d'envoi - Publication fiable d'événements détaillée dans la partie

Il s’agit de Échantillon du projet (Mostlylucid.SegmentCommerce) - une petite démonstration du commerce électronique qui montre les modèles de base. IlM SK3 est intentionnellement simplifié pour montrer clairement les conceptsMSC4 mais il inclut aussi des modèles d’infrastructure réellesMNK5 boîte à l’outboxMRK6 manipulation des messages fondée sur la gestion des messagesMMK7 traitement des tâches en bas de page MNK8 indexation JSONBMEK9 que vous échellez horizontalement dans la productionMZK11

Pensez à cela comme "patternes de production dans un seul organisme -facteur de forme d’application

Choix technologiques

Nous avons choisi un simple, pile cohésive pour garder l’échantillon clair tout en démontrant les patrons de production.

PostgreSQL + pgvector: Base de données unique

Nous utilisons plutôt des bases de données vectorielles distinctes, que des chaînes de messagesM SK1 et des couches de caching, 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 pour les schémas souples (pas de NoSQL distincte)
  • pgvector pour embeddings (no QdrantM SK1Pinecone)
  • LISTEN/NOTIFY pour les emplois (no RedisM SK1RabbitMQ)
  • Tsvector pour le texte complet-texte M SK1pas de recherche élastique)

Un pool de connexion. Une sauvegarde. Un déploiementM SK2

HTMX + Alpine .jsM SK2 Server-UI dirigé

<!-- Instant search (no page reload) -->
<input 
    hx-get="/api/search" 
    hx-trigger="keyup changed delay:300ms" 
    hx-target="#results" />
  • HTMX: Le serveur rend des partiaux (pas de JSON
  • Alpine.js: Réactivité sans étape de construction
  • Amélioration progressive: fonctionne mieux sans JS

SPA-comme UX avec serveur-render simplicitéM SK2

ASP.NET Core: Patterns distribuables

App unique, mais échelle des modèles:

  • Encadré d'envoi ( Événements DBM SK1 → échange pour bus de message
  • Écart d'emploi (LISTENM SK1NOTIFY) → Swap for worker pool
  • Collecteur de séances → échange pour API distinct

Démarrer simple. Distribuer au besoin.

Ce que nous avons évité

  • Aucune base de données distincte sur les vecteurs (pgvector is colocatedM SK1
  • Aucun cadre JS (HTMX + Alpine est plus simpleM SK2
  • Aucun microservice encore (les modèles fonctionnent en monolith ou distribué)
  • Aucun Docker Composez une expansion (une DB,une appM SK2

Le défi Zero-PII

Les systèmes traditionnels de surveillance des utilisateurs stockent l’information identifiable: les nomsM SK1 les courriels , les identificateurs d’utilisateurs liés au comportement. les règlements GDPR et la protection de la vie privée rendent ce problème de plus en plus problématiqueMST4 Notre approche est différenteM ST5

Nous entreposons des modèles de comportement, non des identitésM SK1

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

Le point de vue clé: Vous n’avez pas besoin de savoir Où quelqu’un est pour connaître ce qu’il intéresse.

Aperçu de l'architecture

Le système comporte trois couches de base:

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. Collecteur de séances - Capture des signaux de comportement visualisations M SK2 clics en-à la mémoire seulement
  2. Profil persistant - Fait augmenter les intérêts computés base de données-appuyé
  3. Service de segmentation - Évalue les profils en fonction des règles

La distinction critique: Les séances sont éphémères . Les profils sont persistantsM SK2 Ce n'est pas un détail de mise en œuvre-il s'agit d'une décision sur l'architecture de la protection des renseignements personnels

Profils de session: Strict InM SK1Memory (LFU CacheMSC3

Les séances sont strictement dans la mémoire. Ils vivent dans IMemoryCache avec l'expiration progressive et ne jamais toucher la base de données.

Il s’agit d’une contrainte architecturale difficile: les données de session ne peuvent pas persister . Il recueille des signaux pendant une visite et évacue après 30 minutes d’inactivité par l’entremise de la LFU.

SessionProfile: Le modèle de mémoire In-

// 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; }
}

Entreposé dans IMemoryCache (ou IDistributedCache) avec expiration des Sliding

// 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
            }
        }
    }
});

Constraints durs:

  • Nulle persistance accidentelle: Séances en direct seulement dans le cache (IMemoryCache ou IDistributedCache comme Redis)
  • Éviction des LFU: faibleM SK1utilisation des séances évacuer d'abord sous pression de mémoire
  • L'expiration des Sliding: 30 minutes de la dernière activité
  • Rappel d'évacuation: La dernière chance d’augmenter les signaux de valeur élevés, avant qu’ils ne soient perdus
  • Aucune récupération: Remarque de l'application = toutes les sessions perdues (à moins d'utiliser IDistributedCache, mais encore éphémériqueM SK4

Décision d’élévation (En ce qui concerne l’évacuation)

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;
}

Pourquoi le rappel d'expulsion?

Il s’agit de seulement point de sécurité pour décider de la persistance. Au moment où le cache évacue la session:

  1. Nous connaissons l'historique complet de la session
  2. Nous pouvons évaluer l'engagement total
  3. Nous évitons d’entreposer des sessions à faible valeur-value M SK1visualisation unique de la page, bouclage )
  4. Nous garantissons que les sessions ne persistent pas accidentellement

Si nous n’éleverons pas le ' pendant l’expulsion, la session est a disparu pour toujours. Il s'agit du modèle

Contexte de la session: Ce que nous suivons M SK1Safely)

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"
}

Veuillez noter ce qui non pas ici: adresses IPM SK1 agents d'utilisateurs, URLs complètes , pixels de suiviMSC4 Nous captons Modèles de contexte, renseignements non identifiablesM SK1

Quels sont les signaux?

A signal est un fait comportemental capté au cours d’une action de l’utilisateur.

Ce concept provient de signales éphémères-shortM SK1faits vivants qui existent dans une fenêtre délimitée et vieillissent naturellement . Dans ce système, les opérations de , émettent des signaux comme "api.rate_limited" ou "gateway.slow" pour coordonner le comportement sans couplage étroit.

Nous appliquons ici le même modèle au comportement de l'utilisateur:

  • Vue du produit → product_view signale (poidsM SK1 0.10)
  • Ajouter au cart → add_to_cart signale (poidsM SK1 0.35)
  • Achat → purchase signale (poidsM SK1 1.00)

Les signaux sont d'une durée indéterminée (expire avec la session Numéro de référence : (sans identitéM SK1 et Évaluation (intent-awareM SK2

Étiquetage des empreintes digitales du client (ZeroM SK1Identification des cookies)

Pour relier des sessions sans cookies de suivi, nous utilisons client- empreintes digitales latérales. Le navigateur calcule un hash à partir des signaux seulement le hash à /api/fingerprint.

Le serveur alors HMACs qui ont fait l’objet d’un avec une clé secrète, rendant le site -scopié et inutilisable ailleurs .

Ce code est adapté à mon Projet de détection des robots, où il est utilisé ' pour identifier les écrasiers. La même techniqueM SK3 à des fins différentesMSC4

// 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-side:

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

Nous disposons maintenant d’un site stable, identifiant sans cookies ou localStorage. Voir le empreinte digitale entière.js source (adapté à partir d'une méthode la plus claireM SK1détection de l'obus).

Types de signaux et poids

Les différentes actions ont des niveaux d’intention différents. Nous les modélissons avec TÉU, TPAC, TPMD, TM, TMÉU, TC:.

Types de signaux: Hiérarchie du poids

// 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);
    }
}

Pourquoi cela importe:

  • Vue de page unique (0.01) a remportéM SK1 ne domine pas le signal
  • Ajout au cart (0.35) est un signal d'intention forte
  • Achat (1.00) est le signal le plus fort

Cette hiérarchie empêche de polluer le profil "drive-en parcourant les pages "

SessionCollector: Signales d'enregistrement M SK1Cache-OnlyMSC3

// 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;
}

Rapide parce:

  • Pure dans-mémoire (pas de fichiers DB écritsM SK2
  • Aucune surcoût de serialisation (IMemoryCache)
  • Aucun appel de réseau (cache locale)

Profils persistants: Signales élevées

Lorsqu’une session montre une forte intention (cart ajouteM SK1achats), nous soulignons les signaux à un profil persistant.

PersistentProfileEntity: The LongM SK1Term Profile

// 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;
}

Néanmoins zéro PII:

  • ProfileKey est un hash HMAC (non réversibleM SK1
  • IdentificationMode nous indique comment il a été identifié.
  • Toutes les données sont signales de comportement, pas de renseignements personnels

Elevation: Session M SK1 Profil

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);
}

Lorsque l’élévation se produit:

  • Après le cart ajouter (intensité élevéeM SK1
  • Après achat (conversion)
  • Lorsque l'utilisateur opte pour la persistance ( empreintes digitales / témoins de visiteM SK2login)

Définitions des segments: La rémunération

Nous ' avons chassé les segments pour deux parties . Maintenant, laissez-nous Résultat opérationnel de toute cette collecte de signaux, ils répondent : "qui est ce type d’acheteur?

Pourquoi une adhésion floue?

La segmentation traditionnelle est binaire.

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

Un client ayant fait des achats de 4 est traité d’une manière identique à celle d’un client ayant effectué un achat avec 0.

Segmentation floue donne à chaque profil une note (0-1):

Acquisitions Binaire MSC2 Note floue MSC3
0 ≥ Non
2 ≥ Non
4 ≥ Non
5+ ≥ Oui

Maintenant, vous pouvez personnaliser proportionnellement les clients.

Structure de définition des segments

// 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
}

Types de règles: Ce que vous pouvez vérifier

Chaque règle évalue une dimension du profil:

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

Opérateurs de règles

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
}

Méthodes de combinaison des règles

Comment les règles multiples se combinent pour obtenir un résultat final:

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

Pondération est le plus courant-il vous permet de dire "affaires d’intérêt dans la catégorie 60%, affaires relatives à la recevabilité 30%, questions liées à l’affinité avec la marque

Exemples de segments réels

Voici les segments par défaut du projet de l'échantillon:

1. Enthusiastes de la technologie

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" 
        }
    ]
}

Exemple d’évaluation:

  • Le profil a interests.tech = 0.72, affinities.gadgets = 0.31, affinities.electronics = 0.15
  • Règle 1: 0.72 ≥>= ≤0.4 ==→=points 1.0 ±(+excède le seuil
  • Règle 2: 0.31 ≥>= ≤0.2 ==→=points 1.0 ±(+excède le seuil
  • Règle 3: 0.15 ≥< ≤0.2 ±→ cote 0.75 ◦
  • Finale: M SK1 ≥+ ≤1.0×0.2 ==+ == 0.75×0.2) ==MSC6 == MSC7 == msc8 0.95
  • 0.95 >= ≥0.35 seuil → Membre avec 95% confiance

2. Abandonateurs de cart

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" 
        }
    ]
}

Cela attire le "adds à la cart, mais ne l’achemine pas.

3. HighM SK1Value Clients

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" 
        }
    ]
}

4. Les chasseurs à la négociation

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" 
        }
    ]
}

Tous les segments par défaut

L’échantillon comprend les segments 10 couvrant des modèles de commerce communs d’e-commerce

Segment Icon MSC2 Règles clés SMC3 Usage ESC4
Très élevéeM SK1Customers de valeur 3 4 5 acheteurs M SK6 dépenses élevées , récente traitement VIP 9 programmes de fidélité 10
Enthusiastes de la technologie MSC2 3 Intérêt technologique 4 Affinité avec les gadgets 5 Recommandations techniques 6
Mode à l’avant-garde MSC2 3 Intérêt de la mode 4 Visites multiples 5 Recommandations sur le style 6
Bargain Hunters MSC2️ SMC3 Basse gamme de prixMSC4 préfère les offres ≥ Notifications de vente
Nouveaux visiteurs 2 3 4 Sessions 5 Aucun achat 6 Débarquement 7 Premier 8 Offres d'achat
Abandonateurs de cart 🛒 3 4 le cart ajoute 5 quelques achats 6 Recupération des courriels
Enthusiastes de la maison MSC2 3 Intérêt de la famille SMC4 activité récente SMC5 croisement des produits de maison SMC6 vente MSC7
Activité de la condition physique MSC2 MSC3 Intérêts sportifsMSC4 Caractéristiques sanitaires SMC5 Concentration sur les produits de conditionnement physique
Clients loyaux MSC2 3 4 Acquisitions MSC5 6 Sessions Maintien 8 Récompenses 9
Chercheurs MSC1 MSC2 3 Signales élevés , Browse extensivement 5 Outils de comparaison 6 Info détaillée 7

SegmentService: Membres d'informatique

L'article 5 de la Loi sur l'immigration et le statut des réfugiés SegmentService Évalue les profils en fonction de toutes les règles du segment:

// 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"
        }
    };
}

Expliquebilité Établie-In

Tous les résultats de l’adhésion comprennent : valeurs réelles qui a mené à la détermination du score

// 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" }
    ]
}

Les utilisateurs peuvent voir exactement pourquoi they're in a segment. Ceci est essentiel pour la transparence et la conformité aux Règles de l’Organisation des Nations Unies relatives à la protection des renseignements personnels

Flux de signaux: Fin

Voici' comment une vue de produit devient un membre de segmentM SK1

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

Paramètre de la boîte d'envoi (Preview)

Toutes les actions importantes se déroulent dans le cadre de la Méthode de la boîte d'envoi- notre mécanisme d'orchestrement primaireM SK1

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

Pourquoi? Les données et les événements commerciaux sont consignés dans une seule transaction. Il est possible de perdre les évènements'il n'y a pas de perteM SK2 Des erreurs peuvent être réexaminées automatiquement avec un retour exponentiel

// 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

Partie 3 couvre la mise en oeuvre complète de l’outbox: la liste des tâches, LISTENM SK2 NOTIFY for instant pickup , retry logic

Quoi' Suivant

Cette partie couvre

  • Architecture du profil Zero-PII (sessions éphémères vs profils persistantsM SK1
  • Définitions de segments avec des règles ponctuelles d'appartenance
  • Le service de segmentation avec une explicabilité built-

Partie 3 va plus loin dans

  • Mise en œuvre du modèle de la boîte d'envoi - Publication fiable d'événements et routier unique
  • Écart d'emploi avec PostgreSQL SKIP LOCKED pour le traitement distribué
  • LISTEN/NOTIFY pour un emploi immédiat (pas de sondageM SK1
  • UI et transparence - le tableau de bord et l’explorateur des segments

Les segments sont le résultat opérationnel de ce système.

Finding related posts...
logo

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