UK Travel Recommendation is designed around a single architectural principle: keep the heavy lifting — vector storage, similarity search, and profile arithmetic — inside the database layer, and keep the mobile client as thin as possible. The result is a three-tier system where a React Native / Expo frontend communicates exclusively through a stateless REST API, which in turn delegates all vector operations to a PostgreSQL 15 instance running theDocumentation 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.
pgvector extension. There are no external ML inference services at request time; embeddings are pre-computed offline and stored alongside the attraction records, so every recommendation query is resolved in a single SQL call enriched by in-process MMR re-ranking.
Component Overview
Backend API
Django 6 + Django REST FrameworkTwo apps —
users and recommendations — expose RESTful endpoints under /api/user/ and /api/recommendations/. Business logic (profile updates, MMR re-ranking) runs in Python; authentication is handled by djangorestframework-simplejwt.Vector Database
PostgreSQL 15 + pgvectorEvery attraction and every user profile stores a 777-dimensional
finalVector column. The pgvector extension provides an IVFFlat index and the CosineDistance operator, turning kNN queries into standard SQL that PostgreSQL can execute efficiently.Mobile Frontend
React Native + Expo SDK 54NativeWind provides Tailwind-style utility classes. TanStack Query v5 manages server state and background refetching.
axios sends authenticated requests with JWTs stored in Expo Secure Store. The primary UI is a swipe card feed.Data Flow
The lifetime of a user session follows a clear cycle from registration through continuous profile refinement.Registration and login
The user creates an account via
POST /api/user/register/. The backend creates a User record and an associated UserProfile with a zero-initialised finalVector. On login (POST /api/user/token/), the server returns a short-lived access token (15 min) and a long-lived refresh token (30 days). Both are persisted in Expo Secure Store on the device.Initial preference setup
Before the swipe feed is shown, the onboarding flow asks the user to select preferred attraction categories. These selections are encoded using the same Multi-Hot Encoding scheme used for attractions and used to seed the user’s
finalVector, giving the recommendation engine a meaningful starting point before any swipes have occurred.Recommendation fetch
The frontend calls
GET /api/recommendations/ (optionally with ?county=, ?region=, or ?country= geo-filter parameters). Django retrieves the caller’s UserProfile.finalVector, runs a pgvector cosine-distance kNN query against the attractions table, and then applies MMR re-ranking in Python before returning a ranked list of attraction objects.Swipe interaction
Each swipe gesture triggers either a
POST /api/recommendations/like/<id> or POST /api/recommendations/dislike/<id> request carrying the attraction ID. The backend fetches the swiped attraction’s finalVector and updates the user’s profile vector — blending it toward (like) or orthogonally rejecting it from (dislike) the attraction’s embedding — then re-normalises and persists the updated profile.Profile update and next batch
With the profile updated, TanStack Query invalidates the recommendations cache, and the next fetch returns a freshly computed batch that reflects the user’s latest expressed preferences. This cycle repeats for every swipe, creating a continuously tightening feedback loop.
Vector Representation
Every attraction in the database is characterised by a single 777-dimensionalfinalVector that captures three complementary signals at different levels of abstraction. User profiles are stored in exactly the same format, which means the dot-product geometry that underlies cosine similarity works identically for attraction-to-attraction and user-to-attraction comparisons.
The vector is assembled by concatenating three normalised, weighted sub-vectors:
| Sub-vector | Dimensions | Source | Weight |
|---|---|---|---|
labelMHE | 9 | Multi-Hot Encoding of attraction category labels | 1.5× |
labelEmbed | 384 | Sentence-transformer embedding of the attraction type string | 1.0× |
summaryEmbed | 384 | Sentence-transformer embedding of the attraction summary text | 0.5× |
labelMHE reflects the observation that broad category (e.g. castle, beach, museum) is the strongest predictor of personal taste, while the semantic summary embedding captures finer-grained distinctions at a reduced influence.
The assembly is performed by the normalize() function, called once during data ingestion and once each time a user profile is updated:
safeL2Normalize divides a vector by its L2 norm (guarding against zero-norm edge cases) before the weight is applied. The resulting 777-dim vector is not re-normalised after concatenation — the differential weighting is preserved in the final magnitude.
Recommendation Pipeline
Step 1 — kNN candidate retrieval
Step 1 — kNN candidate retrieval
The pipeline begins with a
The query uses the
pgvector cosine-distance query. The value of k (the number of candidates retrieved) scales with the specificity of the geo-filter in order to keep the candidate pool large enough for MMR to operate effectively:| Geo filter | k |
|---|---|
| No filter (all UK) | 100 |
| Country (England / Scotland / Wales / NI) | 75 |
| Region | 50 |
| County | 25 |
<=> pgvector cosine-distance operator and orders results ascending (smaller distance = more similar), excluding attractions the user has already swiped on.Step 2 — MMR re-ranking
Step 2 — MMR re-ranking
Raw kNN results often cluster around a single corner of the embedding space — for example, returning 20 variations of “Victorian seaside pier” for a user who swiped right on one pier. Maximal Marginal Relevance (MMR) addresses this by iteratively selecting the next recommendation to maximise a trade-off between relevance to the user profile and dissimilarity to the already-selected items.The trade-off is controlled by At
lambda_mult = 0.5, which weights relevance and diversity equally:lambda_mult = 0.5 the algorithm is equally willing to sacrifice a small amount of relevance to gain meaningful diversity. The final ranked list (the top 10 items from the MMR pass) is what the frontend receives.Step 3 — Serialisation and response
Step 3 — Serialisation and response
The re-ranked attractions are serialised by Django REST Framework into JSON objects containing the attraction’s metadata fields (name, county, region, summary, image URL, coordinates) but not the raw vector. The response is paginated so the frontend can request additional batches as the user works through the swipe stack.
Authentication Flow
UK Travel Recommendation uses JSON Web Tokens (JWT) viadjangorestframework-simplejwt. The token lifecycle is designed to balance security (short-lived access tokens) with a smooth user experience (automatic silent refresh).
- Token issuance
- Authenticated requests
- Automatic token refresh
Access tokens are intentionally short-lived (15 minutes) to limit the blast radius of token theft. Because the refresh flow is fully automatic, users are never prompted to log in again during an active session, even if the session spans multiple days.
Infrastructure
The entire backend stack is defined in a singledocker-compose.yaml at the repository root:
backend service reads its configuration from ./backend/.env (Django secret key and database credentials). The db service receives its PostgreSQL credentials via inline environment variables sourced from the same .env file at the shell level. The pgvector/pgvector:pg15-bookworm image ships with the pgvector extension pre-compiled, so no additional installation step is needed — the extension is enabled by the Django migration that creates the vector columns.