SHOW ME WHAT YOU GOT: A Primer on Recommendation Systems in an AI World (English)

You buy a kettle, and the shop spends the next fortnight recommending kettles. You browse a birthday present, and suddenly your entire personality is scented candles. Meanwhile, the thing you actually came back to find is three pages down.

Recommendation systems are supposed to help us choose. To understand why they sometimes get that so wrong, we need to understand what they observe, what they remember, and how they turn that evidence into a list of things to show us.

Shopping is a familiar example of a broader problem. A failing distributed system also presents choices: inspect database waits, compare recent deployments, or trace a slow dependency. As evidence arrives, which diagnostic action is relevant changes. The candidates are actions rather than products, but we still need to represent the current situation, find useful matches, and rank them against an objective.

The architecture we're building towards is a general-purpose relevance engine. Given evolving evidence, a set of candidates, and a defined objective, it determines what is relevant now. Recommendation is one application. The reusable machinery is evidence accumulation, live state, matching, and constrained selection; each domain supplies the meaning of its signals and candidates.

This primer explains those pieces: collaborative filtering, embeddings, vectors, centroids, retrieval, ranking, and streaming recommendation. The central idea is a space in which positions represent relationships. Once we can place items and observations in an appropriate space, we can find neighbours, summarise interests, and track how behaviour changes while someone is still browsing.

Introduction

For a recommendation system, relevance becomes a deceptively simple question:

Given what we know now, which available things should we show next?

Those things might be products, articles, videos, jobs, or suggested actions in an application. The inputs might be explicit preferences, a search query, previous purchases, or a few interactions during the current session.

You do not need a chatbot to do this. You need a way to represent the evidence, find plausible candidates, and put them in a useful order.

The same separation matters for diagnostic actions. Ranking "inspect connection-pool saturation" as useful does not execute it, establish the root cause, or authorise a production change. Relevance selects what deserves attention; application policy governs what happens next.

The basic loop:

  1. Observe something useful.
  2. Update what the system knows about the current situation.
  3. Find relevant candidates.
  4. Apply constraints and rank them.
  5. Observe what happens next.

The timing matters. A recommendation made from yesterday's profile can miss what someone is trying to do this afternoon. A system that updates during the session can respond to the product they just opened or the filter they just changed.

The approach has two useful capabilities: streaming updates keep the current state responsive, while new signal contributors let us extend what the system observes and learn which evidence matters.

Together, these support an emergent streaming recommender. Patterns are discovered from observations, given readable names, and used to inform recommendations. Reusable embedding models and small LLMs help us represent and interpret those patterns without naming every audience segment in advance.

We'll use a shop to make the mechanics concrete. Keep the diagnostic example in mind: the important question is which parts of this machinery depend on selling products, and which parts apply wherever new evidence changes what is worth considering next.

This is a standalone introduction. If you've read my RAG primer, some of the vector machinery will be familiar. If you've read the behavioural inference article or the Stylo.Bot articles, you'll recognise the wider pattern: accumulate partial evidence over time, then use it to make a bounded decision.

What Is a Recommendation System?

Imagine a bookshop assistant. You tell them you enjoyed a particular novel. They might suggest another book by the same author, something with similar themes, or a book that other readers of that novel enjoyed.

Those are different ways of finding a recommendation. The assistant also checks whether the book is in stock, remembers that you asked for a paperback, and avoids offering the book already in your hand.

Software has to make those steps explicit.

flowchart LR
    E[Evidence and current context] --> C[Find candidate items]
    I[Available items] --> C
    C --> F[Check eligibility]
    F --> R[Rank and diversify]
    R --> D[Display recommendations]
    D --> O[Observe response]
    O --> E

Candidate generation finds a manageable pool of plausible items. Ranking decides their order. Eligibility determines what can be offered at all.

A waterproof jacket might be a strong match for someone browsing hiking equipment. It still should not appear as an available purchase if their size is out of stock.

Recommendation, Search, and Prediction

These overlap, but they ask different questions:

System Question Example
Search What matches this explicit request? "Waterproof walking boots under £120"
Recommendation What might be useful in this context? Boots related to the products currently being compared
Classification Which known kind of behaviour does this resemble? A session mostly exploring outdoor equipment
Prediction What outcome is likely next? Which item the visitor may add to their basket

Search gives us a query. Recommendation often has to construct one from behaviour.

A recommendation can rank usefulness without predicting a purchase. Those are different objectives, even when both produce a numerical score.

The Warehouse-and-Analytics Approach

Before looking at algorithms, it helps to picture the infrastructure that often surrounded them. In a warehouse-centred personalisation system, a substantial part of the work happened before a recommendation reached the website.

The organisation collected product metadata, purchase records, browsing events, campaign responses, and customer attributes. Data pipelines cleaned and joined those sources. Analysts defined useful measures and audiences. Models or rules turned the resulting features into recommendations, which were published somewhere the application could read quickly.

AWS's Customer 360 architecture illustrates that broad integration pattern: consolidate sources, construct customer profiles, perform analysis, and feed recommendations or next actions into applications. It includes near-real-time components too; a warehouse does not dictate a single processing schedule.

First, Describe Everything

A shop might maintain data such as:

Data Examples Purpose
Product metadata Category, brand, material, price band, intended use Describe and filter items
Customer attributes Account history, loyalty tier, stated preferences Describe the customer
Interaction history Views, searches, purchases, campaign clicks Record what happened
Derived analytics Spend over 90 days, category affinity, purchase frequency Summarise the history
Audience definitions Frequent outdoor buyers, lapsed customers Turn measures into groups for action

These are illustrative fields, not requirements of every recommender. Collaborative filtering, for example, can learn useful relationships from interactions without an elaborate product taxonomy.

ML aside: a feature is an input. "Price" is a product attribute; "number of times the visitor lowered the price filter" is a behavioural feature. Feature engineering means choosing and calculating the inputs that might help a decision. A signal is a particular piece of evidence supplied to the system.

Still, the metadata-heavy version has a familiar maintenance problem. Someone has to decide which attributes exist, make sure suppliers populate them consistently, and resolve whether "rainwear", "waterproof clothing", and "wet-weather gear" refer to the same thing.

Someone also has to decide what "interested in outdoor products" means. Three views this month? One purchase this year? A threshold over a category-affinity score? The representation and the audience definitions become things teams must maintain.

Then, Build the Processing Pipeline

ETL means extract, transform, load: collect data from source systems, reshape it, and put it in an analytical store. ELT loads it first and performs transformations there. Either way, joins and transformations turn operational records into features that an analytical model can use.

flowchart TB
    P[Product catalogue] --> E[Ingest, clean, and join]
    C[Customer and order records] --> E
    B[Browsing and campaign events] --> E
    E --> W[Warehouse or data lake]
    W --> A[Analytics and feature tables]
    A --> S[Segments, models, and batch scoring]
    S --> R[Published profiles or recommendation lists]
    R --> U[Application serves recommendations]
    A --> D[Reports and experiments]

This is an illustrative batch-oriented arrangement. AWS's batch recommendation pipeline example provides a concrete implementation using data preparation, model training, batch inference, and workflow orchestration.

That separation buys us something valuable: expensive joins and calculations can run without making someone wait at the checkout. Historical data supports reporting, experimentation, and learning patterns that no single session could reveal.

A Fast Response Can Still Use Old Evidence

Imagine an hourly or nightly profile refresh. At 10:00, a visitor starts comparing walking boots. At 10:03, they add a pair to their basket. The application can fetch its existing recommendation list in milliseconds, but that list may still reflect yesterday's interests.

Serving latency and evidence freshness are different measurements. A fast lookup does not tell us when the underlying interpretation last changed.

Precomputation also need not mean serving one fixed list. A system can use the current product or basket to select precomputed related items, then apply current filters. Adobe's recommendation algorithm documentation describes this split between offline computation and serving-time keys and filtering.

What Changes in the Approach Here?

We move a useful part of the interpretation into the running system. An observation updates compact live state; new signal contributors can add evidence; the decision policy can learn which contributions help for a particular context and objective.

Nor is this a claim that older systems were all nightly batch jobs. Netflix's 2013 architecture article already distinguishes offline, nearline, and online computation and explains how to combine them. The trade-off remains relevant: do expensive shared work separately while keeping current-context decisions responsive.

For our small application, the important consequence is that a complete warehouse, a permanent customer profile, and a manually designed audience taxonomy need not be prerequisites for useful adaptive recommendations. We can start with the catalogue, observations, and bounded state, then add historical analysis where it earns its cost.

The Main Ways to Recommend Things

That is the surrounding infrastructure. Now let's look at the different ways a recommender can choose its candidates.

Popularity and Rules

"Best sellers this week" is a recommender. So is "show accessories compatible with the selected camera".

Popularity gives us a useful starting point when we know little about the visitor. Rules express things the system must honour: compatibility, availability, delivery region, or an explicit budget.

These are useful components even in a system with sophisticated machine learning. They also make good baselines. If the clever model cannot improve on a relevant best-seller list, we need to know that.

Content-Based Recommendation

Content-based recommendation uses the properties of the items themselves.

If you browse lightweight hiking boots, the system looks for products with related descriptions, attributes, or tags. This can work with a new product before anyone has bought it, provided we have enough descriptive information.

The representation might be hand-built features such as category, weight, and material. It might be a learned text embedding. Often it is a combination.

Its limitation is that resemblance does not cover every useful relationship. Another pair of boots may be an alternative. Socks may be a complement. Those are different recommendations.

Collaborative Filtering

Collaborative filtering uses patterns in people's interactions.

If people who engage with item A also tend to engage with item B, that relationship can provide evidence for recommending B. The items need not have similar descriptions.

A camera and a memory card are a good example: the relationship can emerge from how people use or buy them together.

Amazon's 2003 item-to-item collaborative filtering paper describes building recommendations around relationships between items. This also matters historically: real-time recommendation existed well before today's language models.

Collaborative approaches need interaction evidence. A new item with no history is harder to place, and a frequently shown item has more opportunities to collect interactions than one nobody sees.

ML aside: the cold start. A new visitor has no history; a new product has no interactions. Both are cold-start problems. They need different fallbacks: product descriptions can help place a new item, while an explicit preference or the first few actions can help with a new visitor.

Latent Factors and Learned Embeddings

Instead of retaining an enormous table of every possible user-item relationship, a model can learn compact vectors for users and items. A scoring operation, often a dot product, estimates how well they fit.

The dimensions are latent factors: numerical patterns learned from the data. Matrix Factorization Techniques for Recommender Systems explains this family of methods and how it incorporates interaction evidence.

ML aside: "latent" means inferred. We did not give the model a column named "likes rainy weekends outdoors". It learned patterns that help explain interactions. Those patterns may combine several human concepts, so an individual coordinate does not necessarily have a tidy name.

Text embeddings are another kind of learned representation. Their training objective differs: similarity in language is useful evidence, but it is not automatically the same as purchasing behaviour.

Hybrid Systems

A hybrid combines several sources of evidence. It might retrieve candidates from a content index, add items suggested by interaction patterns, and include popular products when personal evidence is weak.

The ranking stage then weighs those candidates against the current context.

Approach Main evidence Useful strength Common limitation
Popularity Aggregate activity Works with little personal history Misses individual intent
Content-based Item properties Can place new descriptive items Similarity misses some useful relationships
Collaborative Interaction patterns Finds relationships beyond descriptions Needs sufficient interaction evidence
Hybrid Several sources Covers more situations Requires clear combination and evaluation

Streaming is a separate choice. It describes how incoming evidence updates the system. A content-based or collaborative recommender can incorporate streaming updates; a hybrid can combine live state with models trained offline.

What Is a Vector, Actually?

A vector is an ordered list of numbers.

[0.21, -0.45, 0.08, 0.73, ...]

Those numbers become useful when we define what produced them and how to compare them.

An embedding represents something as a position in a chosen multidimensional space. The useful part is the arrangement: the relationships between positions encode something we care about, such as similarity of meaning or similarity of behaviour.

Think of a map. A coordinate is useful because it lets you locate one place relative to others. With embeddings, the space might arrange descriptions of walking boots near descriptions of hiking footwear, with coffee makers in another neighbourhood. We can then ask which items lie near a query or a summary of someone's interests.

Unlike a geographical map, the axes need not have familiar names. A learned representation spreads information across many coordinates. Its training objective determines which relationships the geometry is encouraged to preserve.

This is why "turn text into numbers" only tells half the story. Arbitrarily assigning each product a number would also produce numbers. An embedding is useful when its structure supports the comparisons we want to make.

For this article, there are two ways to construct the space worth separating.

Measured Behavioural Features

We can build a vector directly from observations:

[views per minute, fraction of visits to comparison pages, basket additions]

Each position has an explicit meaning. Building that vector requires a few choices about units, scales, and missing values.

This is the kind of feature engineering you encounter in behavioural classification. My Behaviour, Not Identity article develops that idea for web traffic.

Turning Features into a Vector

Start by deciding what one vector describes. For example, take the last five minutes of one browsing session. Within that window, suppose we observed ten product views, four comparison actions, three price-filter changes, at least one basket addition, and a mean observed dwell time of 30 seconds.

Choose a fixed order and calculate a number for each position:

Position Feature Example transformation Value
1 Views per minute Divide by 20 and cap at 1 0.10
2 Comparisons per product view Divide 4 by 10 0.40
3 Price-filter changes Divide by 5 and cap at 1 0.60
4 Any basket addition Yes = 1, no = 0 1.00
5 Mean observed dwell time Divide seconds by 120 and cap at 1 0.25

That gives us:

session behaviour = [0.10, 0.40, 0.60, 1.00, 0.25]

No language-model call was needed. We measured events and encoded them. The divisors are illustrative engineering choices, not learned constants or recommended defaults. Comparisons per view can exceed one, so its cap or treatment also needs defining for real traffic.

Feature scaling gives inputs usable numerical ranges. Without it, dwell time measured in seconds could dominate a feature expressed as a fraction merely because its numbers are larger. Scaling does not establish which feature is most predictive; that is a separate question for weighting and evaluation.

The order, window, units, and transformations are the vector's schema. Position 2 must mean the same thing in every vector we compare. Missing observations also need an explicit policy: an unmeasured dwell time is different from a measured zero. We might exclude that comparison, impute a value and retain a missing-value indicator, or keep the feature in a separate contributor.

Categories need an encoding too. For an unordered category, assigning mobile = 1, desktop = 2, and tablet = 3 invents a numerical ordering. A one-hot encoding gives each category a position: mobile becomes [1, 0, 0], desktop [0, 1, 0], and tablet [0, 0, 1]. That represents membership; it does not discover that two categories are more alike than a third.

These summaries also discard event order. Markov-style transition features would be a natural first implementation for capturing it, using the same approach described in Stylo.Bot's transition modelling.

Choose a small set of event states, such as view, compare, change filter, and add to basket. Count consecutive transitions and turn each row of counts into a distribution over what happened next:

P(add to basket | compare)
    = count(compare → add to basket) / count(all transitions out of compare)

Those transition frequencies can become vector features alongside activity rates and interest summaries. Updating the counts only requires the previous state and the new event. Keep support counts too: one observed transition is much weaker evidence than hundreds, and an unseen source state needs a fallback rather than division by zero.

ML aside: the Markov assumption. A first-order model estimates the next state from the current one. It captures local order, but does not preserve the entire journey. If the previous two actions matter, we can define a richer state or use a higher-order or learned sequence model.

That gives us an incremental, inspectable starting point before reaching for a sequence encoder. The important choice is what information the representation should preserve.

Semantic Embeddings

A text embedding model transforms something, such as a product description, into a position in a learned space:

"Lightweight waterproof boots for muddy walking trails"
    ↓ embedding model
[0.21, -0.45, 0.08, 0.73, ...]

For a semantic similarity model, related descriptions should occupy similar positions. That gives us a way to retrieve by meaning, including when two descriptions use different words.

Where BERT and RAG fit. Sentence-BERT adapts Google's BERT to produce sentence vectors suitable for similarity comparisons. Facebook AI's original RAG work combines vector-based passage retrieval with generation. Our connection is the retrieval geometry: use a representation to find related candidates. The recommender can display those candidates without generating an answer.

A live interest vector can be constructed by combining the embeddings of items the visitor engages with. That gives us a content-based query built from behaviour.

The coordinate system must match. Two models producing 384 numbers do not necessarily produce comparable vectors. Model version, preprocessing, pooling, and normalisation belong to the representation's definition. A jointly trained pair of encoders can also be compatible, as in dense retrieval, without being identical encoders.

Measured behavioural features and semantic embeddings describe different things. We can combine their scores through policy, or deliberately learn a shared representation. We cannot assume their raw coordinates are interchangeable.

Colour or Root Vegetable

Colour makes the distinction concrete. Russet means a reddish-brown colour, so there is a linguistic relationship to both red and brown. A text embedding model may capture that relationship from its training. It does not follow that its distance between "russet" and "red" measures how similar two fabrics will look.

We have several possible questions, each needing a different representation:

Question Useful representation
Does this product belong in the red filter? A defined catalogue colour family
Which swatches look similar? Measured colour coordinates and a colour-distance measure
Does "warm autumnal reddish-brown" describe this product? A semantic text embedding
Is this visitor increasingly exploring warm colours? A behavioural summary of colour interactions

For catalogue membership, we can map supplier labels to canonical families and encode those categories. Russet might belong to a reddish-brown family, with membership in red or brown filters determined by the catalogue policy. One-hot colour categories make different families distinct, but do not encode shade similarity.

For visual similarity, use an actual swatch or measured colour. RGB already supplies three coordinates, but ordinary distance in RGB does not reliably track perceived differences. Spaces such as Oklab aim for approximate perceptual uniformity, making distance more useful for comparing shades. The W3C colour specification explains that representation. A supplier's "russet" label alone does not uniquely determine the product's swatch.

A colour space is already a useful example of the idea. A shade has coordinates, and a suitable distance tells us something about its relationship to another shade. We did not need a text embedding model to create that numerical representation.

Text embeddings fit when the relationship is linguistic, but language brings ambiguity. "Russet" also appears in potato varieties such as Russet Burbank. Depending on the model and its training, the bare word might sit near "potato" or "baking" as well as colour terms. A perfectly reasonable association for potatoes could give us a useless jacket recommendation.

"Colour: russet, a reddish-brown shade" supplies context that identifies the intended sense. "Russet potato suitable for baking" supplies a different context. Short queries can still work well with a suitably trained retrieval model; Sentence Transformers distinguishes short-query retrieval from matching texts of similar length. The issue is the intended meaning and the model's task, rather than length alone.

Representation is a modelling decision. We choose which aspect of an observation to preserve and which relationships should matter. An embedding model, a colour space, and a transition matrix each encode different evidence. Their value depends on the question we need to answer.

The same observation can support several valid interpretations, but only some are relevant to the decision we're making.

The same issue appears in semantic ledgers and document extraction: a correctly parsed address can be the correspondence address when we need the property address. In behavioural inference, an observed action can support several possible explanations. In each case, we retain the observation, its context, and the evidence connecting it to the role or decision we care about.

Trusting any one representation as a complete account of the situation can hide important distinctions. Combining contributors gives us a way to preserve those distinctions and resolve conflicting evidence through policy. Several vectors derived from the same ambiguous word can also repeat the same mistake, so adding representations only helps when they contribute useful information.

How Do We Measure Similarity?

Cosine similarity measures how closely two vectors point in the same direction:

cosine(a, b) = dot(a, b) / (length(a) * length(b))

If both vectors have been normalised to length one, this reduces to their dot product.

ML aside: normalising a vector. Here, it means rescaling the whole vector to length one while preserving its direction. That differs from scaling individual features. A dot product multiplies corresponding coordinates and adds the results; with unit-length vectors, it gives us the cosine score directly.

A higher cosine score means closer alignment in that representation. A score of 0.9 does not mean a 90% chance of buying something. Nor does it guarantee that items are interchangeable or compatible. The interpretation depends on the representation and its purpose.

Cosine is one choice, not a rule for every vector. The colour example needs a colour-distance measure; a behavioural space needs a metric that respects its feature scales and meaning. Normalising everything indiscriminately can discard magnitude that matters, such as overall activity or colour lightness.

For a small catalogue, comparing a query with every item may be sufficient. Larger catalogues can use an approximate nearest-neighbour index: a structure that searches for likely close matches without comparing every item. It reduces search work in exchange for potentially missing some of the true nearest neighbours.

A vector database is one way to manage this. It is not what makes recommendation possible: the important operation is retrieving candidates by an appropriate similarity measure.

What Is a Centroid?

A centroid is an average position.

If several vectors describe items someone has shown interest in, their average gives us a compact summary of that interest. With different weights for different interactions, it becomes a weighted centroid.

Let's use an intentionally tiny, invented two-dimensional space:

Trail shoes A:     [1.0, 0.0]
Trail shoes B:     [0.8, 0.6]
Headphones:        [0.0, 1.0]

These are teaching values, not actual model output. Both shoe vectors are closer to each other than either is to the headphones.

Suppose someone views shoes A twice, with weight 1 per view, then adds shoes B to their basket, with weight 3:

weighted sum = 1 × [1, 0] + 1 × [1, 0] + 3 × [0.8, 0.6]
             = [4.4, 1.8]

total weight = 5
centroid     = [4.4, 1.8] / 5
             = [0.88, 0.36]

We can normalise that centroid and use it to retrieve candidate products. The choice of weights is our starting policy, not a universal truth about how much a basket addition means.

A Centroid Is Not a Cluster

Clustering assigns observations to groups. A centroid summarises a group that has already been chosen.

One session can maintain a centroid without running K-means over the whole customer population. If we want emergent audience segments, a separate clustering process can discover groups of similar profiles and calculate their centroids.

This is where segmentation and recommendation meet:

  • Segmentation asks which observed behaviours belong together.
  • Recommendation uses the current evidence to retrieve and order candidates.

A segment can contribute evidence, but a visitor need not wait for a population-wide clustering pass before receiving a recommendation.

Why One Average Can Mislead

Someone can shop for walking boots and a birthday present in the same session. Averaging all those interactions may produce a point between the interests that represents neither well.

One response is to retain several interest summaries, each with its own weight and freshness. MIND (2019) explores multiple user-interest vectors with a learned routing mechanism. A simpler implementation can maintain a small bounded set of centroids and use thresholds to decide which one an interaction should update.

More centroids are not automatically better. We need enough evidence to support them, a limit on how many survive, and rules for merging or expiring them.

We can now describe an interest as a position, summarise it, and keep separate interests apart. The next question is how to update those summaries as new events arrive. We'll return to discovering shared communities once the live loop is in place.

How Streaming Recommendation Works

A common architecture collects interactions, processes them periodically, rebuilds profiles or models, then serves recommendations from the result. That can be sensible for expensive population-level learning.

A streaming architecture additionally maintains state as observations arrive.

flowchart TB
    subgraph Catalogue[Catalogue preparation]
        P[Product descriptions and attributes] --> E[Generate item representations]
        E --> I[Store catalogue vectors]
    end
    subgraph Live[Live recommendation loop]
        A[Interaction arrives] --> V[Validate and extract signals]
        V --> U[Update current interest state]
        U --> Q[Retrieve candidates]
        I --> Q
        Q --> F[Enforce eligibility and rank]
        F --> D[Display results]
        D --> O[Observe exposure and response]
        O --> A
    end

The distinction is where adaptation happens. An accepted event can change live state without requiring a model retraining run first.

Phase 1: Prepare the Items

For a content-based shop, we might embed each product's name, description, and useful attributes. Store the resulting vector alongside a product identifier and its representation version.

Regenerate the representation when meaningful product content changes. Keep transactional facts such as current stock available separately so eligibility checks can use current data.

This preparation can happen before the visitor arrives. Viewing a product then requires a vector lookup rather than another embedding-model invocation.

Phase 2: Turn Events into Evidence

A product view, comparison, basket addition, purchase, and explicit dislike carry different meanings.

A view might be curiosity or an accidental click. A purchase is stronger evidence of a completed transaction, but may also mean the immediate need is satisfied. Repeated views should not be allowed to overwhelm every other signal without limit.

We therefore need an event policy: which actions contribute, how strongly, within what window, and to which interest state?

For explicit negative feedback, maintain an exclusion or separate negative signal. Feeding arbitrary negative weights into a positive weighted mean can make its total weight approach zero and destroy the interpretation of the centroid.

Phase 3: Update a Small Amount of State

The simplest unweighted running mean is:

newMean = oldMean + (newVector - oldMean) / (count + 1)

It gives every observation equal influence in the final average. As history grows, a new interaction moves the mean less and less. That is often undesirable when we care about current intent.

A moving average gives recent evidence more influence. The EWMA statistic uses this form:

newState = (1 - alpha) * oldState + alpha * observation

A fixed alpha forgets according to the number of updates. For sessions arriving at different rates, elapsed-time decay can be more useful.

ML aside: half-life is a memory setting. With a 30-minute half-life, an observation has half its original influence after 30 minutes and a quarter after an hour. Here it describes the weight of old evidence, not the chance that someone is still interested.

For example, retain a weighted vector sum and its total evidence mass, meaning the accumulated weight of the supporting events:

decay       = 0.5 ^ (elapsedTime / halfLife)
newSum      = decay * oldSum + eventWeight * itemVector
newMass     = decay * oldMass + eventWeight
newCentroid = newSum / newMass

This is an illustrative positive-signal policy. Initialise the sum and mass at zero, require positive event weights and half-life, and avoid normalising an empty or zero vector. In a real stream, define how late and out-of-order events are handled.

The vector update needs work proportional to the vector dimension. It does not grow with the number of historical events contributing to the summary.

Phase 4: Retrieve and Rank

Use the current interest direction to retrieve similar items. Then apply the rest of the recommendation policy.

Candidates can come from several sources: semantic similarity, item relationships, current basket compatibility, or popularity. Different sources have different score distributions, so combining raw scores needs care. A learned ranker, calibrated scoring rules, or rank fusion can provide an explicit combination strategy.

Hard constraints remain hard constraints. An explicit maximum price should not become a weak preference that an unrelated relevance boost can override.

After filtering, ensure enough eligible candidates remain. The nearest ten items are no help if all ten are unavailable.

A Session, Step by Step

Consider a visitor looking for boots for a wet weekend away. This walkthrough is illustrative behaviour for the proposed system, not a measured implementation result.

Observation State interpretation Possible recommendation response
Arrives with no history Little personal evidence Relevant popular items or a request for preferences
Opens lightweight walking boots Initial outdoor-footwear interest Similar boots across a useful price range
Applies a £120 maximum Explicit constraint Eligible products within the budget
Compares waterproof boots Reinforced current interest Candidates matching the wet-weather context
Adds one pair to the basket Stronger evidence; one item selected Reduce duplicate alternatives; consider compatible extras
Opens headphones for a gift Possible separate interest Start or update another interest summary

The system is updating an interpretation of the visit. It is not discovering a permanent personality from six clicks.

A simplified application flow could look like this. These are illustrative application interfaces, not an existing NuGet API:

// Update the session from an interaction already accepted by the application.
await interests.ObserveAsync(sessionId, interaction, cancellationToken);

// A subsequent read uses the current state.
var state = await interests.GetAsync(sessionId, cancellationToken);
var candidates = await matcher.FindAsync(state, request, cancellationToken);
var recommendations = policy.Select(candidates, request);

The implementation must define what "subsequent" guarantees. In one process, the next read can observe an awaited update. With multiple nodes or an event queue, freshness depends on routing, processing lag, and state consistency.

Real time needs a measurable contract: how long after an accepted interaction can recommendation reads still return the old state?

Forgetting Needs More Than Normalisation

Suppose an interest has no new supporting events. After one half-life, its sum and mass halve; after two, they are one quarter of their earlier values.

But dividing the decayed sum by the decayed mass gives the same centroid. Normalising the sum also preserves its direction. The system still points towards that old interest.

To make evidence actually expire, inspect its decayed mass and age when reading the state. Below a chosen threshold, retire the interest or fall back to less personal recommendations.

That decay can be calculated lazily on reads and updates. We do not need a job that touches every idle session every second.

Keep mass separate from confidence. Many repeated events can create a large mass without providing independent evidence that the interpretation is correct.

What Streaming Removes, and What Still Runs Separately

Streaming updates let the live path use a new event without replaying the full history, rebuilding every profile, or waiting for a population-wide model refresh.

They do not eliminate the rest of the system's work.

Work Typical placement in this approach
Look up an existing item's vector Live interaction processing
Update an interest summary Live interaction processing
Retrieve and rank current candidates Recommendation request
Embed a new or changed product Catalogue preparation
Discover population-level segments Separate consolidation or clustering
Train or evaluate an outcome model Separate learning and evaluation
Persist state and maintain indexes Defined storage and maintenance paths

A pretrained embedding model can remain fixed while a visitor's state changes continuously. This is online state adaptation. Updating model parameters from incoming interactions is a separate form of online learning.

In ML terminology, training learns model parameters from examples; inference applies the resulting model to an input. Looking up an existing item vector and updating a session centroid can happen without doing either operation again for that item.

State adaptation is sufficient for the streaming content-based baseline described here. Learning model parameters can be added separately when the evidence justifies it.

From Web Traffic to Any Observation Stream

We can now return to the broader architecture from the opening. Stylo.Bot classifies web traffic observed over time. Its inputs are evidence about requests and client behaviour. The more general pattern is:

observations → features → evolving state → comparison → decision

The stream does not have to consist of web requests.

Domain Observations Possible output
Ecommerce Views, filters, comparisons, basket events Relevant products
Publishing Reads, searches, saves, topic feedback Articles to explore
Applications Feature use, navigation, completed tasks Relevant help or next actions
Distributed systems Latency, errors, traces, deployment events Next diagnostic actions
Devices Measurements and operating events State classification or maintenance candidates

Each domain supplies its own feature extraction, grouping key, and decision policy. A grouping key might identify a session, device, account, or workflow. The reusable part is the processing pattern, not a universal embedding in which all traffic already has meaning.

My behavioural inference article describes the wider argument. Learning to Get Faster explores incremental statistics in Stylo.Bot, and The Compression Property explains how summaries retain useful evidence without retaining every observation at full detail.

Stylo.Bot also performs asynchronous absorption and consolidation. Reusing the principle does not require copying its exact timing or detection policy into recommendation.

Matching Is Easier Than Predicting the Future

A nearest-centroid classifier can say that current behaviour resembles a known pattern. A matcher can find items near the current interest state.

To predict an outcome, we need evidence relating state to what happened next. One simple starting point could retain transition counts between defined states; another could train a model against later interactions.

Movement through a vector space can supply features for those models. Extrapolating that movement alone does not establish that someone will purchase a particular product. Any claimed probability needs evaluation and, where appropriate, calibration.

This distinction keeps the architecture useful: matching can deliver value before we build a reliable next-action predictor.

Emergent Recommendation: Discover the Patterns

The streaming loop gives us an evolving picture of one session. The behavioural inference pattern also lets us discover recurring relationships across sessions, without defining every audience segment in advance.

This is the connection to the Leiden techniques in Stylo.Bot. Represent observed sessions as behavioural vectors, connect sufficiently similar neighbours, and use Leiden community detection to discover communities in that graph.

Observed sessions → behavioural vectors → similarity graph
                                              ↓
                                       Leiden communities
                                              ↓
                                  representative behavioural patterns

ML aside: a graph is a set of connections. Each session is a node; an edge connects sessions we consider similar. An edge weight records the strength of that connection. A community is a group with a useful pattern of connections; Leiden discovers such groups.

Leiden operates on the graph. We calculate centroids or select representative members after discovering the communities. A community with an irregular shape may need several representatives rather than one average.

For example, a community might contain sessions that compare several alternatives, repeatedly narrow the price range, and then select a mid-priced item. Another might contain sessions that quickly choose a product and move on to accessories. Those descriptions come from inspecting the evidence; they are interpretations of discovered patterns, not labels we had to assign beforehand.

The recommendation emerges when we connect a behavioural pattern to useful item or outcome evidence. For a live session resembling the first community, we might retrieve alternatives that similar sessions found useful. For the second, compatibility-aware accessories might be better candidates. We still need evidence to support those choices and evaluation to establish that they help.

The behavioural centroid itself is not a product query in a text embedding space. The bridge is an association: community membership links to observed item interactions, outcomes, or a separate summary of product interests.

There are two timescales here. Community discovery refreshes the shared reference patterns. Streaming updates move a live session relative to those patterns as events arrive. A recommendation can use the latest available communities without waiting for Leiden to run again, and direct interest matching remains available when no community fits well.

"Emergent" still involves design choices: features, neighbour selection, edge weights, and Leiden's resolution setting, which controls the granularity of the grouping, affect what communities appear. It means the memberships arise from observed relationships, rather than a manually enumerated list of audience rules.

That deserves a later article of its own: building the graph, maintaining useful representatives, and testing whether community evidence improves recommendations beyond the simpler streaming baseline.

Describing Discovered Patterns with a Small LLM

We can also give a discovered cluster a short name and a small description explaining why that name fits: what behaviour the cluster represents and which observed properties distinguish it. A small local LLM can be suitable for this bounded task: it receives computed values and an explanation of what each property displays. Model choice still needs testing against representative clusters.

An unexplained centroid such as [0.72, 2.4, ...] gives the model little to work with. Instead, supply named features, units, sample support, and a baseline for comparison. For learned embeddings whose coordinates have no explicit meaning, use representative members and their observable properties rather than inventing names for latent dimensions.

Here is an illustrative prompt:

Give this discovered cluster a short, readable name.
Write a brief description explaining why the name fits the observed behaviour
and what the cluster represents.
Use each property's meaning, value, and baseline to explain the differences.
Describe observed actions; do not infer demographics, motives, or causes.
If the supplied evidence is insufficient, say so in the description.
Return JSON with exactly two fields:
name: 2–5 words.
description: 1–2 sentences grounded in the supplied properties.

The input could be this small JSON document. These are teaching values, not measured results:

{
  "cluster_id": "cluster-17",
  "cluster_version": 7,
  "session_count": 200,
  "observation_window": "last 24 hours",
  "baseline_population": "all observed sessions in the same window",
  "features": [
    {
      "id": "comparison_share",
      "value": 0.72,
      "baseline": 0.18,
      "unit": "mean fraction per session",
      "displays": "Fraction of product views with at least one comparison action. Higher means more comparison activity."
    },
    {
      "id": "price_narrowing_count",
      "value": 2.4,
      "baseline": 0.6,
      "unit": "mean count per session",
      "displays": "Number of times the visitor lowered the maximum price filter during the session."
    }
  ]
}

For example, a grounded response would be:

{
  "name": "Compare and narrow",
  "description": "The name reflects two behaviours that occur more frequently in this cluster than in baseline sessions: product comparison and lowering the maximum price filter."
}

The statistics discovered the pattern; the LLM supplied a name and an interpretation people can read. Validate the response structure and claims against the supplied evidence. Retain the name and description with their cluster version, supporting values, and model version, and refresh them when the underlying pattern materially changes. This can happen outside the live recommendation path.

That description adds a semantic annotation to the behavioural graph. If we also retain explicit relationships between clusters, observations, candidates, and outcomes, we move towards a dynamic knowledge graph grounded in the behavioural substrate. The generated description remains an interpretation linked to evidence. It does not become a new observation simply because it is stored in the graph.

This follows the signal-and-evidence approach in Behavioural Inference and Constrained Fuzzy MoM: computed properties carry the evidence, while a bounded model helps interpret it.

Where Recommendation Systems Go Wrong

The System Learns From Its Own Choices

Items shown more often receive more opportunities for clicks. Feeding those clicks back into ranking can amplify the system's earlier choices.

Record exposure alongside response: which items were shown, in what positions, and under which policy? Lack of a click on an unseen item tells us nothing about whether someone would have liked it.

Diversity and controlled exploration can help gather broader evidence. They need their own evaluation rather than an assumption that every novel result is useful.

One Signal Becomes a Permanent Label

A gift purchase is not necessarily an enduring preference. An accidental click is weaker evidence than an explicit request. A completed purchase can mean "stop recommending this exact thing".

Short-lived session interests, decay, separate interests, and visible reset or feedback controls help the system recover from those misunderstandings.

Keeping state local to a session also reduces the need for long-lived profiles. It does not make behavioural data automatically anonymous: what the system retains and how it links observations still matter.

More Adaptation Becomes More Instability

If one click completely replaces the current interpretation, recommendations thrash. If years of history dominate, they barely move.

Update weights, short and longer time horizons, and minimum evidence thresholds control that trade-off. Explicit user instructions should take precedence over a weak inferred preference.

A Small Summary Becomes an Unbounded Service

One centroid is small. Millions of abandoned profiles are not.

Limit active profiles, interests per profile, retention, pending events, and any saved history. Give duplicated events a defined treatment so retries do not manufacture interest. Finding and Fixing Unbounded Growth covers the operational discipline behind this.

How Do We Know It Works?

A list changing after a click demonstrates responsiveness. It does not establish that the recommendations improved.

We need to measure both relevance and operation.

Recommendation quality: compare against simple baselines such as popularity, item-to-item matching, and one weighted centroid. Also inspect coverage, diversity, and whether explicit constraints were honoured.

ML aside: the alphabet soup of ranking. K means the number of results we inspect. Recall@K measures how many relevant items made that list. NDCG@K rewards putting relevant items near the top, with higher rewards for stronger relevance when graded judgements are available. Both depend on how we define "relevant".

Streaming behaviour: measure time from event acceptance to visibility in recommendation reads, update cost, retrieval latency, processing lag, and state size. Define what survives a restart and how the system behaves while state is unavailable.

Evaluate with time-ordered data. At each decision point, the system should only use information available then. Embedding models or item relationships trained using future interactions can leak future knowledge even when the session replay itself is ordered correctly.

Historical logs reflect the old recommendation policy, so offline results have limits. Controlled live experiments help assess whether the new system improves the outcome we actually care about.

That objective should be explicit. More clicks, a completed task, a useful purchase, fewer returns, and long-term satisfaction are different outcomes. Optimising one does not guarantee the others.

Learn Which Signals Matter

Suppose we begin with product-interest vectors, then add a contributor measuring comparison behaviour. To integrate it, define its source, representation version, scaling, comparison method, and missing-value policy. Changing the dimensions of an existing representation requires compatible state or a migration.

Its importance can depend on context: price-filter changes may help while someone compares alternatives, while compatibility becomes more useful after they select a product. A weighting or ranking mechanism can learn those contributions from outcome feedback. Adding vectors or running Leiden alone does not establish their predictive value.

A useful check is to repeat an evaluation with one contributor removed. If recommendation quality stays the same, that signal may be redundant in that context. If it falls, we have evidence that the contributor was doing useful work. This is called an ablation study.

Constrained Fuzzy MoM describes the shared contracts for contributors. Constrained Fuzziness explains why learned scores remain subject to explicit budget, availability, and compatibility constraints.

Why This Is Interesting Now

A small application can use pretrained representations without training a language model from scratch, then use a bounded local model to describe patterns discovered in its own traffic. That reduces the work needed to experiment with richer evidence and make the results inspectable.

The practical opportunity is to start small, add contributors, measure their effect, and retain useful discoveries. Streaming recommendation and community detection have long histories; the combination with reusable models makes this an interesting time to build on the behavioural inference approach.

Part 2: Recommend the Next Diagnostic Action

Part 2 will start with a failing distributed system. Requests are slowing down, errors are increasing, and a deployment happened recently. The candidates are diagnostic actions: examine database wait statistics, inspect connection-pool metrics, compare deployment versions, or trace a dependency.

As new measurements arrive, the system updates its state and reconsiders which action could provide useful evidence next. Each candidate needs a domain-specific description of when it helps, its preconditions, and its cost. Historical cases or explicit matching policies can connect those candidates to the current state; putting everything into arbitrary vectors would not supply that relationship.

We will test which contracts carry across to diagnosis and which need domain-specific implementations. That should give us a concrete basis for the generalised NuGet library.

A later ecommerce example will still give us an approachable way to inspect changing interests and product recommendations. The customer intelligence series and existing Mostlylucid.SegmentCommerce application provide related material for that work.

Finding related posts...
logo

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