Every registered user has aDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/viet2811/uk-travel-recommendation/llms.txt
Use this file to discover all available pages before exploring further.
UserProfile record that acts as a living representation of their attraction preferences. Rather than storing an explicit list of favourite tags, the profile encodes taste as three numeric vectors that are continuously nudged by every swipe. Liked attractions pull the profile towards similar content; dislikes push it away. This continuous, incremental update strategy means the system learns your preferences gradually without requiring a manual re-configuration, and the first useful recommendations can appear even before onboarding is complete.
Profile Structure
TheUserProfile model (users/models.py) stores three vector fields alongside a OneToOneField link to Django’s built-in User:
labelMHE
9 dimensionsA multi-hot encoding over nine top-level attraction categories (e.g. Historic, Natural, Cultural). Each dimension accumulates positive evidence from likes and is clamped to zero on dislikes, so it always remains non-negative.
labelEmbed
384 dimensionsA sentence-transformer embedding capturing the semantic meaning of attraction type labels (e.g. “Medieval Castle”, “National Park”). Updated via exponential moving average on likes, and deflected away from disliked directions.
summaryEmbed
384 dimensionsA sentence-transformer embedding of attraction summary text, capturing thematic and descriptive content. Updated with a lower learning rate than
labelEmbed, providing a slower-moving representation of narrative preferences.Onboarding
During onboarding, two mechanisms populate the profile before the user starts swiping.Category preferences — POST /api/user/preferences/
The client sends a 9-element
preferences list (the MHE vector) and an optional labels list of human-readable attraction type strings. The server stores the MHE directly and uses the all-MiniLM-L12-v2 sentence-transformer model to embed the label strings; the mean embedding is written to both labelEmbed and summaryEmbed:Visited attraction import — POST /api/recommendations/like/bulk/
The user can import a list of attractions they have already visited. The bulk-like view averages the
labelMHE, labelEmbed, and summaryEmbed fields of all provided attractions. If the profile embeddings are still zero (first import), the averaged embeddings are assigned directly; otherwise they are blended with an alpha of 0.6 towards the new average:Like Update
When you swipe right on an attraction (POST /api/recommendations/like/<id>), the LikeAttractionView._update_profile() method shifts your profile towards that attraction using fixed learning rates:
| Component | Update rule | Learning rate |
|---|---|---|
labelMHE | Additive accumulation | α = 0.2 |
labelEmbed | Exponential moving average | α = 0.15 |
summaryEmbed | Exponential moving average | α = 0.1 |
(1 − α) · old + α · new ensures that labelEmbed and summaryEmbed remain bounded and that recent preferences carry more weight than distant history. labelMHE is purely additive, allowing category counts to grow indefinitely (they are L2-normalised at query time).
Dislike Update
Swiping left (POST /api/recommendations/dislike/<id>) uses a different strategy: rather than simply subtracting the attraction’s vector from the profile, the update uses vector rejection for the embedding components to remove specifically the component of the profile that points towards the disliked item, minimising collateral change to unrelated directions.
Why vector rejection rather than direct subtraction?
Why vector rejection rather than direct subtraction?
Direct subtraction
profile -= α · item would shift the entire profile in a direction that is determined by the magnitude and orientation of the disliked item’s vector. Vector rejection is more surgical: it computes the scalar projection of the profile onto the disliked direction and removes only that component, leaving the profile’s orientation in all other semantic directions unchanged. The effect is that the profile moves away from the disliked content type without inadvertently reducing affinity for unrelated content.np.maximum(0, ...) on labelMHE prevents any category dimension from going negative, maintaining the interpretation that each dimension is a non-negative evidence count.
Profile Delta
EveryUserInteraction record stores a profile_delta float that quantifies how much a single swipe changed the profile:
profile_delta is the cosine distance (not similarity) between the pre- and post-update profile vectors. A value close to 0 means the swipe barely shifted the profile (e.g. confirming an already-strong existing preference); a larger value indicates a meaningful new preference signal. This field serves as a per-interaction learning signal and can be used for analytics or adaptive learning-rate tuning in future iterations.
Reset
Sending aPOST request to /api/user/reset/ zeros out all three vector fields, returning the profile to its initial state as if the user had just registered: