Нульова розумова одиниця (PII) } Частина 2: профілі, сигнали і частки (Українська (Ukrainian))

Нульова розумова одиниця (PII) } Частина 2: профілі, сигнали і частки

Friday, 26 December 2025

//

20 minute read

Вхід Частина 1 Ми обговорювали філософію та серію статей. Частина 1. 1 Мы создали образцовщик данных.

Тепер давайте побудуємо центральну систему.

  1. Архітектура профілю Zero- PI - Ефемеральні сеанси і постійні профілі
  2. Сигнали і ваги - Що ми відстежуємо і як воно накопичується.
  3. Визначення відрізка - Невиразне членство з правилами зваженості (головна подія)
  4. Перегляд шаблонів " Вихідні " - Вірогідне видання подій (докладна частина 3).

Це зразок проекту (Mostlylucid.SegmentCommerceЦе навмисно спрощено, щоб продемонструвати концепції ясно, але це також включає справжні схеми інфраструктури (побудовані ззовні повідомлення, обробка завдань, індексування JSCB), які ви вимірюєте горизонтально у виробництві.

Думайте про це як про "продукти шаблонів в однофункціональному коефіцієнті." Та сама структура коду працює незалежно від того, чи ви працюєте один процес, чи розподіляєте дані за допомогою служб.

Вибір технології

Ми обрали простий, з' єднаний стос щоб утримати зразок ясним, а потім демонструючи його моделі.

PostgreSQL + pgvector: одна база даних

Замість окремих векторних баз даних, черг повідомлень та шарів кешування, ми використовуємо 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 для гнучких схем (не окремої NoSQL)
  • pgvector для вбудовування (без Qdrant/Pinecone)
  • СЛУХА для завдань (немає Redis/RabbitMQ)
  • tsvctor для повного тексту (не для олівця)

Один басейн з' єднання. Одна резервна копія.

HTMX + Billian.js: UI Server- Driven

<!-- Instant search (no page reload) -->
<input 
    hx-get="/api/search" 
    hx-trigger="keyup changed delay:300ms" 
    hx-target="#results" />
  • HTMX: Сервер передає частини (без JSON → імітація)
  • Альпійський.js: Реактивність без кроку збирання
  • Прогресивне поліпшення: Працює без JS, краще з ним

SPA- подібний до UX з простотою виконання серверів.

Ядро ASP.NET: Розкішні візерунки

Шкала окремих програм, але шаблонів:

  • Вихідні (DB подій) → Поміняти місцями на поштовий автобус
  • Черга завдань → Помінятись на робочий басейн
  • Збірник сеансів → Свопінг на окремий API

Розпочати просто. Розподіл за потреби.

Чого ми уникали

  • Немає окремої векторної бази даних (пgvector з' єднується)
  • Без оболонки JS (HTMX + альпійський простіший)
  • Ще немає мікрослужби (взірці працюють у моноліті або розподілені)
  • Без розрідження Докера Комоса (одна DB, одна програма)

Проблема з нульовим PI

Традиційні правила стеження за сховищами користувача, які можна розпізнати: імена, електронні листи, ІД користувачів, пов' язані з поведінкою. Правила GMR і конфіденційності роблять це все складнішим. Наш підхід до роботи відрізняється:

Ми зберігаємо поведінку, а не ідентичність.

flowchart LR
    subgraph "Traditional Approach"
        User1[User: john@email.com] --> 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

Головне розуміння: Вам не потрібно знати, хто така ВООЗ, щоб знати, в чому вони зацікавлені..

Огляд архітектури

Система складається з трьох основних шарів:

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. Колектор сеансів - Захоплює поведінкові сигнали (диви, клацання, воз додається) - лише в пам' яті
  2. Постійний профіль - Підвищення високоцінних сигналів, обчислення інтересів - Бажана база даних
  3. Служба відрізків - Обчислення профілів проти правил, призначення членів

Критична різниця: Сеанси є ефемеральними. Профілі є постійними. Це не є докладними деталями } Це питання про архітектуру приватності.

Профілі сеансу: Strict In- Memory (LFU Cacher)

Сеанси: строго в пам' ятіВони живуть у IMemoryCache з застарінням і ніколи не торкатись бази даних.

Це жорстке архітектурне обмеження: дані сеансів не можуть залишатися незмінними. Він збирає сигнали під час відвідування і виселення через 30 хвилин бездіяльності через правила кешування LFU (Стару часто використовувану) під тиском пам' яті.

SessionProfile: модель пам' яті

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

Збережені у IMemoryCache (або IDistribedCache) з можливістю використання накладених даних

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

Складні обмеження:

  • Нульова випадкова наполегливість: Сеанси живі лише у кеші (IMemoryCache або IDistribedCache, наприклад, Reredis)
  • Виселення: Невикористані сесії виселення спочатку під тиском пам'яті
  • Нахиляючий термін: за 30 хвилин від останньої дії
  • Зворотний виклик Eviction: Останній шанс піднести високоцінні сигнали до втрати
  • Поновлення неможливе: Перезапуск App = всі програні сеанси (якщо не використовувати IDistribedCache, але все ще ефемеральний)

Рішення висоти (On Evication)

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

Навіщо виселятися?

Це лише безпечний пункт для визначення наполегливості. До часу, коли кеш викине сеанс:

  1. Ми знаємо всю історію сесії.
  2. Ми можемо оцінити загальну помолвку.
  3. Ми уникаємо збереження сеансів низької вартості (погляд на сторінки, перестрибування)
  4. Ми гарантуємо, що сесії не будуть випадково продовжуватись

Якщо ми не підвищимо рівень під час виселення, то частина: не завершено назавждиЦе і є план.

SessionContext: те, до чого ми слідкуємо (безпечно)

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

Зауважте, що неDescription of a condition. Do not translate key words (# V1S #, # V1 #,) тут: IP- адреси, агенти користувача, повні адреси URL, стеження за пікселями. Ми перехоплюємо контекстні шаблони, не розпізнана інформація.

Що таке сигнали?

А сигнал це поведінковий факт отриманий під час дії користувача. замість того щоб відстежувати "хто," ми стежимо "що сталося."

Ця концепція походить від ефемеральні сигналиУ цій системі дії випромінюють сигнали на кшталт: "api.rate_limited" або "gateway.slow" координувати поведінку без жорсткого зчеплення.

Тут ми застосовуємо той самий шаблон до поведінки користувача:

  • Перегляд продуктуproduct_view сигнал (вагома: 0. 10)
  • Додати до візкаadd_to_cart сигнал (вагома: 0, 5)
  • Купитиpurchase сигнал (вагома: 1. 00)

Сигнали: ефемераль (доклади з сеансом), нуль- PI (без профілю) і вагований (нерозбірливо).

Відбиток клієнта (Ідентифікація Zero-Cokie)

Для посилання сеансів без кук, ми використовуємо відбитки з боку клієнта. Переглядач обчислює хеш від сигналів (часовий пояс, роздільна здатність екрана, засіб для показу веб- GL, відбиток полотна) і відсилається тільки хеш до /api/fingerprint.

Тоді сервер HMACs, що хеш за допомогою секретного ключа, який робить його захищеним та непридатним для використання в інших місцях.

Цей код адаптується від мого Проект виявлення botТам, де вона використовується, щоб ідентифікувати скребків, ту ж техніку, іншу мету.

// 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 HMACs the client hash with a secret key
var profileKey = HMACSHA256(clientHash + secretKey);

Тепер у нас є стабільний ідентифікатор сайту без печива або локальної Сторжування. повноцінне джерело відбитків відбитків. js (взято з maculid.bot-детекторації).

Типи сигналів і ваги

Різні дії мають різний рівень призначення. базові вагиunit synonyms for matching user input.

Типи сигналів: Ієрархія ваги

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

Чому це є важливим:

  • Перегляд окремої сторінки (0.01) не буде домінувати сигнал
  • Додавання до візка (0.35) є сильним сигналом наміру
  • Купити (1.00) є найсильнішим сигналом

Ця ієрархія забороняє " навігацію " за допомогою " забруднювати профіль.

CessionCollector: Сигнали запису (Тільки кеш)

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

Швидка через:

  • Чиста пам' ять (без запису DB)
  • Без послідовності (IMemoryCache)
  • Немає мережних викликів (локальний кеш)

Постійні профілі: підвищені сигнали

Коли сеанс показує високий намір (карт додає, купується), ми підносимо сигнали до постійний профіль.

ПостійністьProfileEntity: профіль довгої таблиці

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

Ще нуль PII:

  • ProfileKey є хеш HMAC (неможлива)
  • IdentificationMode повідомляє нам про те, як його було визначено (fingerprint/cookie/login)
  • Всі дані системні сигнали, без особистої інформації

Висота: Сеанс → Профіль

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

Коли відбувається підвищення:

  • Після додавання візка (з високим наміром)
  • Після покупки (конверсія)
  • Якщо користувач надає перевагу (fingprint/ cokie/ login)

Визначення сегменту: Виплата

Ми дражнили сегменти двох частин, а тепер доставимо їх. вивід придатного для дій З усіх цих сигналів колекція каже: "Що це за крамниця?"

Чому невиразне членство?

Традиційна сегментація є бінарною: ви або у сегменті, або ні. Це створює проблеми:

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

Клієнт з 4 покупками сприймається однаково до 1 з 0. Це неправильно.

Невиразне сегментування надавати кожному профілю оцінку (0-1):

Дівчатаunit synonyms for matching user input |-----------|--------|-------------|

  • 0 * No00 0.00 Дзвінок 2: Ні. ♪4' No8] 5 + * Так 1. 0' ї_BARBAR.

Тепер ви можете персоналізувати пропорційно: "майже високоцінні" клієнти отримують трохи інше лікування, ніж "ніде поруч."

Структура відрізка

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

Типи правил: що ви можете перевірити

Кожне з правил визначає один розмір профілю:

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

Оператори правил

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
}

Методи комбінації правил

Об' єднати декілька правил з остаточним рахунком:

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

Вага Найбільш поширеним є те, що ви можете сказати: "категорія відсоткової ставки має значення 60%, послідовність має значення 30%, прив'язаність бренду має значення 10%."

Приклади справжніх відрізків

Ось типові сегменти у проекті прикладу:

1. Технологія

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

Приклад обчислення:

  • Профіль має interests.tech = 0.72, affinities.gadgets = 0.31, affinities.electronics = 0.15
  • Правило 1: 0. 72 >= 0. 4 → рахунок 1. 0 (поріг за винятком)
  • Правило 2: 0, 31 >= 0. 2 → рахунок 1. 0 (поріг за винятком)
  • Правило 3: 0, 15 < 0, 2 → рахунок 0, 75 (партамін: 0, 15/ 0. 2)
  • Остаточний: (1.×0.6 + 1.×0.2 + 0. 75×0.2) / 1. 0 = 0.95
    1. 95 >= 0. 35 поріг → Член з довірою 95%

2. Відкидувачі від вантажівок

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

Це ставить "додається до возу, але не купується" шаблон, що представляє кампанії відновлення.

3. Клієнти високої вартості

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. Мисливці - варвари

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

Всі типові відрізки

У зразок міститься 10 сегментів зі спільними шаблонами електронного набору:

Д_ д. д. д. д. д. д. д. д. д. д. д. д. д. д. |---------|------|-----------|----------| Д-р Харріс: "Га-Ті-Ті-Ті-Ті-Ті-Ті - митниці" Центова ставка, технічний продукт. Д. д. д. д. д. д. д. д. ст. д. д. д. д. д. д. ст. Д-р Цукер: "Отже, ми маємо на увазі, що ми маємо на увазі, що ми маємо справу з іншими людьми." Нові сеанси, без закупівель, продається перший-пурчаний м'яч. Переводчики: music, music, music, music, music, music, music, music, music, music, music, multiple, music, music, music, multiple, minutes, availion, availue ♪ Home Encehiasts ♪ [Домівка] ♪ [Домівка] Д-р Цукер: "Отже, ми маємо на увазі, що ми маємо на увазі, що ми маємо справу з іншими людьми." Дівчата. 10+сеанси, +. Дівчата. Д_ д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д.

SectionService: Computing членствами

The SegmentService обчислює профілі за всіма правилами сегментів:

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

З' єднання з поясненням

Кожен результат членства включає фактичні значення це призвело до результату:

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

Користувачі можуть точно зрозуміти чому Вони належать до сегменту, що є вирішальним для дотримання прозорості та стандарту ВВП.

Потік сигналу: Кінець до кінця

Ось як перегляд продукту стає членом сегменту:

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

Шаблон теки Вихідні (Preview)

Всі важливі дії проходять крізь Шаблон outboxНаш основний механізм оркестрування:

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

Чому? Ділові дані та події записуються у одній операції. Події неможливо втратити. Невдачі автоматично повторюються з експоненціальним зворотним зв'язком.

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

Частина 3 містить повний опис реалізації вихідних повідомлень: черги завдань, LISTEN/ NOTIFY для негайного завантаження, повторення логіки і шаблонів масштабування.

Що наступне

Ця частина охоплює:

  • Архітектура профілю Zero- PI (реферемеральні сеанси та постійні профілі)
  • Визначення відрізка з чіткими правилами членства і зваженими правилами
  • SectionService з вбудованою можливістю пояснення

Частина 3 занурюється глибше:

  • Реалізація шаблонів " Вихідні " - Впевнені події опублікування і єдиний маршрутизатор
  • Черга завдань з PostgreSQL SKIP LOCKED для розподіленої обробки
  • СЛУХА для негайного звантаження завдань (без опитування)
  • Інтерфейс та прозорість - панель " Ваші інтереси " та дослідник сегментів

Сегменти є виводом цієї системи, який придатний до дії. Вони відповідають "що це за крамниця?" з розмитими партитурами, а не бінарними відрами. І кожен користувач може точно бачити, чому він знаходиться у сегменті.

Finding related posts...
logo

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