Repository navigation
Recommendation and Ranking
Recommendations are what set boxlore apart. They are hybrid: boxlore's private API finds candidate shows and episodes on the server, then the phone re-ranks them for your listening habits. Neither stage works alone.
The repository's recommendation guide is the detailed reference. This page summarises it and links each idea to the code.
- Retrieval, on the server The app sends a bounded set of listening seeds to the API. The API uses semantic matching against the podcast catalog to return candidate shows and episodes. The API's code is private and is not in this repository. The recommendation guide describes it as stateless, with no cloud user-profile store.
-
Re-ranking, on the phone
:core:rankingscores the candidates with a LinUCB-style contextual bandit and a set of taste meters. Both learn from what you do and are stored on the phone inadaptive_ranking_database.
flowchart TB
subgraph Phone_Seeds ["On the phone: build the request"]
S1["Recent listening seeds<br/>(liked, completed, partly played)"]
S2["Subscribed show IDs"]
S3["Recently played episode IDs to exclude"]
end
subgraph Server ["On the server: boxlore API (private)"]
R1["Semantic matching against the catalog"]
R2["Curated daypart rows, trending, search"]
end
subgraph Phone_Rank ["On the phone: re-rank"]
F1["18-slot feature vector per candidate"]
M1["LinUCB-style score blended with the API's prior"]
D1["Diversity: cap repeats per show, genre penalties"]
end
Phone_Seeds --> Server
Server --> Phone_Rank
Phone_Rank --> UI["Home, Explore, Smart Queue"]
The main recommendation call is POST recommendations/v2 in BoxLoreApi. The request (SyncModels, built in PodcastRepositoryRecommendations) carries:
| Field | Limit | What it is |
|---|---|---|
| Seeds | Up to 12 | Recent episodes and shows, each with title, genre and description text. Episode seeds are weighted: liked 1.0, completed 0.9, at least 50% played 0.75, at least 20% played 0.55, otherwise 0.35. Show seeds are weighted 0.35. |
| Subscribed show IDs | Up to 250 | So the API can tell new shows from ones you already follow |
| Excluded episode IDs | Up to 250 | Recently played episodes, so they aren't suggested again |
| Excluded show IDs | Shows to leave out of the results | |
| Interests | Up to 12 | Topic hints, when available |
| Context | Country, content languages, mode and result limit |
Requests also carry the app key and a device ID header. Other surfaces use their own endpoints, for example POST recommendations/because-you-like, POST episodes/similar, GET search/semantic, GET curated/vibe (Home daypart rows) and GET curated/curiosity-v3 (Lore). What we track and why explains what leaves the phone.
AdaptiveCandidateScorer builds an 18-slot feature vector (FeatureSlot in RankingModels) for each candidate:
INTERCEPT, SHOW_AFFINITY, GENRE_AFFINITY, SOURCE_AFFINITY, FRESHNESS, NOVELTY, DURATION_FIT, SUBSCRIBED, RESUME_PROGRESS, UNPLAYED, SERIAL_MATCH, SERVER_RELEVANCE, EXPOSURE_FATIGUE, TIME_CONTEXT, OFFLINE_SUITABILITY, EXPLICIT_PREFERENCE, RECENT_SUBSCRIPTION, CURRENT_SHOW.
Two examples:
-
Freshness decays with episode age:
$\exp(-\text{ageHours} / 336)$ , a two-week time constant. - Duration fit falls off linearly around a 45-minute ideal.
The schema is versioned. A stored model with the wrong dimension is not loaded.
Each objective keeps its own regularised online linear model with optional upper-confidence exploration.
| State | Meaning |
|---|---|
covariance) |
|
inverseCovariance) |
Recomputed with Gauss-Jordan after every update |
rewardVector) |
Accumulates |
updateCount |
Resolved outcomes so far |
Learned weights are
-
$\alpha = 0.15$ . The exploration bonus applies only when the objective allows exploration and$n \ge 50$ . - The prior is the API's relevance score or the app's own deterministic score. It always keeps at least 35% of the weight.
Updates apply a forgetting factor so tastes can drift:
| Objective | Exploration | Typical use |
|---|---|---|
DISCOVERY |
After 50 outcomes | Home recommendations, Explore, Lore |
CONTINUATION |
Limited | Daily Mix (MixtapeEngine), Smart Queue |
YOUR_SHOWS |
None | Your shows; a deterministic score that never uses the bandit |
OFFLINE |
None | Downloads |
RankingRolloutPolicy turns learned re-ranking on for Home by default. Other surfaces keep the API's order or the app's deterministic scores unless re-ranking is enabled for them. Curated Home daypart rows always keep the API's order.
BayesianPreferenceFacet stores evidence for and against a facet and turns it into an affinity between −1 and 1 using a Beta-style prior.
- Facet types:
SHOW,GENRE,SOURCE,DURATION_BUCKET,TIME_CONTEXT,INTENT. Feedback currently updatesSHOW,GENREandSOURCE. - Evidence decays with a 90-day half-life:
$S(t) = S_0 \cdot 2^{-t / 90\text{ days}}$ . A genre you stop listening to drifts back toward neutral. - Genre keys are canonicalised, and the placeholder genre "Podcast" is ignored.
Taste meters also feed the bandit through the affinity features.
When a surface shows a candidate, the engine records a RankingExposureEntity with the feature snapshot. When you act on it, RankingReward turns the action into a reward and the exposure resolves into a model update. If there was no exposure, for example after a deep link, the taste meters still update.
| Action | Weight |
|---|---|
| Subscribe | +0.80 |
| Like | +0.65 |
| Add to queue | +0.55 |
| Download manually | +0.55 |
| Complete | +0.35 |
| Move up in queue | +0.25 |
| Meaningful play | +0.22 |
| Open details | +0.08 |
| Move down in queue | −0.25 |
| Unlike | −0.50 |
| Unsubscribe | −0.70 |
| Early skip | −0.70 |
| Dismiss | −0.75 |
| Remove an autofilled queue item | −0.80 |
Listening time adds up to +0.4 more, based on minutes listened and the fraction of the episode played. The total is clamped to −1 to 1.
- Meaningful play: at least 60 seconds, or at least 20% of the episode.
- Early skip: skipping within 30 seconds, counted only for items the queue added itself or that came from Lore.
- Complete: at least 95% played, the last 5 seconds, or no more than 5 minutes left on an episode of 15 minutes or more.
DiversityReranker shapes the final list:
- Removes duplicate episodes.
- Caps repeats per show:
maxPerShowis 2 by default and 1 for Home's trending grid. - Subtracts 0.08 per repeat of a genre and 0.12 for a recently shown show.
- Can reserve one slot for something new.
There is no separate cold-start mode. With no outcomes the blend weight is zero, so the API's prior decides the order. The learned weight then grows linearly to 65% by 50 outcomes, and exploration starts only after that. Diagnostics label the stages cold_start (no outcomes), learning (fewer than 50) and adaptive.
- Models, taste meters and exposures live in
adaptive_ranking_database, owned by:core:ranking. Exposures are pruned by age and row count. - The library backup includes ranking state (
adaptiveRanking). See Data & Storage Contracts. - Settings > Privacy > Reset recommendations clears the local ranking tables.
- Features overview
- Recommendations & discovery
- Playback
- Podcasting 2.0
- Folders, tags & icons
- Smart Queue & downloads
- Make it yours
- Backup, sync & import
boxlore · Curious. Calm. Yours.