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
Zero PII Intelligence des clients - Partie 2: Profils, Signaux & Segments
Wednesday, 31 December 2025
À 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.
- Architecture du profil Zero-PII - Sessions éphémères vs profils persistants
- Signales et poids - Ce que nous repérons et comment il s'accumule
- Définitions de segments - Adhésion floue avec règles pondérées la manifestation principaleM SK2
- 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
- Collecteur de séances - Capture des signaux de comportement visualisations M SK2 clics en-à la mémoire seulement
- Profil persistant - Fait augmenter les intérêts computés base de données-appuyé
- 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:
- Nous connaissons l'historique complet de la session
- Nous pouvons évaluer l'engagement total
- Nous évitons d’entreposer des sessions à faible valeur-value M SK1visualisation unique de la page, bouclage )
- 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_viewsignale (poidsM SK1 0.10) - Ajouter au cart →
add_to_cartsignale (poidsM SK1 0.35) - Achat →
purchasesignale (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:
ProfileKeyest un hash HMAC (non réversibleM SK1IdentificationModenous 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 LOCKEDpour 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.