User Representation¶
The visitor digital twin: who a visitor is and what they care about, learned from a short survey and from how they behave, then kept current through the visit.
A visitor never states their interests outright. They show them: what they open, how long they linger, what they skip, what they search, how they answer a survey. This area is about turning that behavior into a faithful, explainable model the recommender can use, the Adaptive layer of the DAC architecture.
The model has two halves: stable attributes from a short survey (demographics, goals, persona, personal involvement) that bootstrap it before any behavior exists, and dynamic state learned from interaction (inferred engagement, attention, and a sense of which themes resonate).
What the model actually holds¶
Concretely, the recommender keeps one record per visitor,
UserSignals. It is a small, readable summary,
not raw event logs:
| Part of the model | What it is | Where it comes from |
|---|---|---|
tag_affinity |
the visitor's interests, as weighted facet:label tags (e.g. theme_what:forced labor at 1.0) |
survey answers + the tags of content they engage with |
tag_aversion |
themes to steer away from | content they disliked |
taste_vector |
a single point in "meaning space" standing for their overall taste | the average of the embeddings of stories they liked |
recency_vector |
the meaning of the last story they read | their most recent view |
positives / negatives |
which items they liked or disliked, with a decayed strength | inferred engagement (dwell, completion, revisits, rating) |
demographics |
age, gender, nationality, province | the survey (stored for inspection) |
behavior |
summary stats (how deep, how complete, how fast) | engagement, used for explanations, not scoring |
Tags capture interests by concept; the vectors capture them by meaning, so the model can match content that is related even when it is tagged differently.
In reality¶
Here is one visitor's record after the surveys and a few views, as returned by
GET /api/usermodel. The two vectors are 384 numbers each (from the sentence embedding model),
shown truncated:
{
"user_id": "7QF2",
"tag_affinity": {
"theme_what:forced labor": 1.0,
"theme_how.type_of_stores:personal stories": 0.83,
"person_who.age_group:age 55-64": 0.5,
"place_where.camp_areas:barrack 56": 0.41
},
"tag_aversion": {
"theme_what:daily life": 1.0
},
"taste_vector": [0.021, -0.118, 0.077, "…381 more…"],
"recency_vector": [0.044, -0.090, 0.100, "…381 more…"],
"positives": { "5567": 0.82, "6012": 0.44 },
"negatives": { "5901": 0.30 },
"viewed": ["5567", "5901", "6012"],
"recent_views": ["6012", "5901", "5567"],
"behavior": {
"n_views": 3, "n_positive": 2, "n_negative": 1,
"avg_dwell_ratio": 0.78, "completion_rate": 0.67,
"revisit_rate": 0.33, "depth": 0.67
},
"demographics": {
"age": "55_64", "gender": "female",
"nationality": "france", "personal_connection": "descendant"
}
}
Read it top to bottom: this visitor said they care about forced labor and personal stories
(the strongest tag_affinity weights), skipped a "daily life" story (so it became an
aversion), liked two items and disliked one, and reads fairly thoroughly (avg_dwell_ratio
0.78). That is everything the recommender needs, in one small record.
Taste and recency vectors, in plain terms¶
The two vectors are the one part of the model that is not tags, so they deserve a word.
An embedding is a list of numbers a language model produces from a piece of text to capture its meaning. Picture a map where every story is a pin: stories about similar things sit close together, unrelated ones sit far apart. The individual numbers are not meant to be read by a human; only how close two pins are matters. (Here each pin has 384 coordinates instead of 2, but the idea is the same.)
meaning "map" (a flat sketch of the 384-dimensional space)
● ● forced-labor stories ★ = taste vector
● ● ● ★ (average pin of the
● ● stories this visitor liked)
......................................
○ ○ daily-life stories
○ ○
- The taste vector is the average position of all the pins the visitor liked: a single point standing for their overall taste. To judge a new story, the recommender measures how close that story's pin is to this point. Closer means more like what they enjoy.
- The recency vector is simply the pin of the last story they opened. It powers the "more like the one you just read" nudge, which can point somewhere different from their long-run taste.
This is why the model can recommend a relevant story even when it happens to be tagged differently: closeness on the map is about meaning, not about sharing an exact tag.
How it is used¶
When the app asks for recommendations, each part of the model feeds a scorer that rates every candidate story, and the scores are combined into one ranking (tag matching leads):
flowchart LR
E["visitor events<br>+ survey"] --> M["UserSignals<br>(the user model)"]
M -->|tag_affinity| T["tag match"]
M -->|taste_vector| S["semantic similarity"]
M -->|recency_vector| R["more like the last read"]
M -->|tag_aversion| A["penalty"]
T & S & R & A --> Rank["ranked recommendations"]
So the survey bootstraps the model on day one, behavior sharpens it through the visit, and every recommendation is traceable back to the parts of the model that produced it. The end-to-end story, from opening the app to the Nth recommendation, is in the Pipeline Walkthrough.
What you will find here¶
-
The shared vocabulary of visitor actions and the canonical event schema every source maps to.
-
How events travel from the app through RudderStack to the user model, with analytics kept off the hot path.
API for this area
Visitor events enter through the recsys ingest webhook on the
AI Engine API (POST /api/ingest); the materialized user model
is readable at GET /api/usermodel.

Where the data comes from: the Bergen-Belsen Panoramic Display at the MEMORISE exhibition, which gathered the survey and interaction data behind the user model.