The recommendation engine transforms your accumulated preferences into a ranked list of UK attractions through a three-stage pipeline: your live user profile vector is normalised and used to query a pgvector-backed PostgreSQL database via approximate nearest-neighbour cosine search, the resulting candidate pool is then passed through a Maximal Marginal Relevance (MMR) re-ranker that balances personal relevance against variety, and only then is the final list of ten attractions returned. Every swipe you make updates your profile in real time, so each new recommendations fetch reflects your latest expressed taste.Documentation 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.
Overview
The pipeline can be summarised as follows:Build the user vector
The three components of your
UserProfile — labelMHE, labelEmbed, and summaryEmbed — are L2-normalised and concatenated into a single 777-dimensional finalVector that represents your current preferences.kNN cosine search
pgvector computes
CosineDistance between your vector and every eligible attraction’s finalVector. The top-k results (k varies by geo scope) form the candidate pool. Attractions you have already interacted with are excluded.MMR re-ranking
The candidate pool is re-ranked by Maximal Marginal Relevance to balance similarity to your profile against redundancy within the returned list, producing a diverse set of ten recommendations.
Vector Representation
Each attraction and each user profile is ultimately represented as a single 777-dimensional vector calledfinalVector. It is assembled from three semantically distinct sub-vectors, each weighted to reflect its relative importance:
labelMHE
9 dimensions · weight 1.5A multi-hot encoding over the nine top-level attraction categories (e.g. Historic, Natural, Cultural). The higher weight gives broad category preference the strongest pull.
labelEmbed
384 dimensions · weight 1.0A sentence-transformer embedding of the attraction’s
typeLabel string, capturing fine-grained semantic type similarity (e.g. “Castle” vs “Manor House”).summaryEmbed
384 dimensions · weight 0.5A sentence-transformer embedding of the attraction’s
summary text, capturing thematic and descriptive similarity at the content level.normalize() utility in recommendations/utils.py produces this composite vector. Each sub-vector is independently L2-normalised before being scaled by its weight, ensuring that a component with large raw magnitudes cannot dominate simply due to scale:
normalize() function is applied identically to both attraction vectors (stored in Attraction.finalVector) and user profile vectors at query time, so cosine distance is computed in a shared, consistently-scaled space.
kNN Search
Once the user vector is built,RecommendationsListView queries the database using pgvector’s CosineDistance operator, ordered by descending similarity (i.e. 1 - CosineDistance). The query is constructed dynamically:
k — the number of candidates retrieved before re-ranking — is adjusted based on the active geo scope. A geo-filtered query operates over a smaller, pre-filtered set of attractions, so a proportionally lower k still captures a representative candidate pool while keeping query latency low:
| Geo scope | Query parameter | k value |
|---|---|---|
| No filter | (none) | 100 |
| Country | ?country=<name> | 75 |
| Region | ?region=<name> | 50 |
| County | ?county=<name> | 25 |
county, region, and country columns ensure the WHERE clause applied by each geo filter is evaluated efficiently before the vector scan.
Attractions that the authenticated user has already interacted with (liked or disliked) are excluded from the candidate queryset via
.exclude(interactions__user=user) before the kNN search is performed.MMR Re-Ranking
Returning the raw top-k by cosine similarity would tend to produce a list where several attractions are nearly identical — all Norman castles in the same region, for example. Maximal Marginal Relevance (MMR) addresses this by greedily selecting items that are simultaneously relevant to the user and dissimilar to what has already been selected. The MMR score for a candidate itemi at each selection step is:
lambda_mult = 0.5 the formula weights relevance and diversity equally. The implementation uses cosine similarity over finalVector for the redundancy term, matching the same vector space used for retrieval:
Diversity Tracking
To monitor whether the recommendation engine is producing genuinely varied results over time, the system computes an Intra-List Diversity (ILD) score every 10 interactions. ILD measures the average pairwise dissimilarity across the most recent batch of liked attractions:InteractionView._log_ild() using the finalVector fields of the last 10 interacted attractions and is stored in the UserRecommendationBatch model alongside a timestamp. A rising ILD trend indicates the engine is successfully broadening recommendations; a sustained low value could signal a filter-bubble effect.
Computed every
10 interactions (liked or disliked)
Stored in
UserRecommendationBatch.ild_scoreAttractions already interacted with are excluded from the candidate pool at the database query level, so they can never appear in a new recommendations response regardless of how high their cosine similarity to your profile is.