-
Notifications
You must be signed in to change notification settings - Fork 2
Recommendation and Ranking
boxlore incorporates an on-device personalization system that tailors podcast discovery and automated queue generation to individual listening habits.
The personalization system is structured with a clear division of responsibility between candidate retrieval and on-device scoring:
-
Stateless Candidate API:
External API endpoints serve candidate pools based on public charts, categories, daypart curation, and vector similarity without server-side personalization. -
On-Device Scoring Engine:
The personalization engine runs on the user's Android device inside:core:ranking. Learning models, taste meters, exposure histories, and reward updates are maintained locally in a dedicated database (adaptive_ranking_database) to re-rank candidate feeds in real time.
flowchart TB
subgraph Candidate_Generation ["1. Candidate Generation (Stateless)"]
S1["Local Subscriptions & Unfinished History"]
S2["Stateless Curated Daypart Rails"]
S3["Trending Charts & Editorial Feeds"]
S4["Qdrant Concept Vector Matches"]
end
subgraph Feature_Extraction ["2. Client-Side Feature Extraction"]
F1["Publish Recency Decay"]
F2["Target Duration Fit"]
F3["Show / Publisher Familiarity"]
F4["Decayed Taste Meter Value"]
end
subgraph Scoring_Model ["3. LinUCB Contextual Bandit Scoring"]
M1["Expected Reward: \(\hat{r} = x^T \theta\)"]
M2["Exploration Bonus: \(\alpha \sqrt{x^T A^{-1} x}\)"]
M3["Combined Score: \(Score = \hat{r} + Bonus\)"]
end
subgraph Presentation ["4. Post-Processing & Surface Presentation"]
D1["Intra-Rail Diversity Filters (Max 2 per show)"]
D2["Time-of-Day Rotation"]
D3["Home Screen / Queue Injection"]
end
Candidate_Generation --> Feature_Extraction
Feature_Extraction --> Scoring_Model
Scoring_Model --> Presentation
The ranking engine formulates episode selection as a contextual multi-armed bandit using the LinUCB (Linear Upper Confidence Bound) algorithm.
For each candidate episode, a standardized
-
Recency Score (
$x_1$ ): Exponential decay function based on the episode release timestamp:$$x_1 = \exp\left(-\frac{\Delta t}{\tau_{\text{recency}}}\right)$$ -
Duration Fit (
$x_2$ ): Normal distribution measuring alignment between candidate duration$D_e$ and user mean listening session duration$\mu_D$ :$$x_2 = \exp\left(-\frac{(D_e - \mu_D)^2}{2\sigma_D^2}\right)$$ -
Show Affinity (
$x_3$ ): Decayed exponential taste meter value for the episode's parent podcast. -
Genre Affinity (
$x_4$ ): Decayed exponential taste meter value for the episode's primary genre. -
Completion History (
$x_5$ ): Historical completion ratio of episodes by this publisher.
For a candidate represented by context vector
-
$\theta = A^{-1} b$ : The current weight vector estimating feature importance. -
$A \in \mathbb{R}^{d \times d}$ : The ridge regularized feature covariance matrix ($A = D_x^T D_x + I_d$ ). -
$b \in \mathbb{R}^d$ : The accumulated reward vector ($b = D_x^T r$ ). -
$\alpha \ge 0$ : The exploration hyperparameter. When candidate uncertainty is high ($\sqrt{x^T A^{-1} x}$ is large), the algorithm gives an exploration bonus to discover new genres or creators.
To prevent early listening habits from permanently biasing recommendations, boxlore maintains continuous taste meters across three categorical dimensions:
SHOWGENREPUBLISHER
Each taste facet tracks an affinity score
If a listener stops playing episodes from a specific genre, that genre's affinity naturally decays back to zero over a period of 60 to 90 days.
When an episode is rendered on screen, the engine logs a RankingExposureEntity snapshot recording the features and model state at the moment of display.
| User Action | Signal Type | Reward Value | Engine Response |
|---|---|---|---|
| Play Episode (> 30s) | Positive | Updates model weights; reinforces show and genre taste meters. | |
| Like Episode | Positive | High positive boost to genre and creator affinity. | |
| Subscribe to Show | Positive | Maximal positive reinforcement for the podcast and category. | |
| Complete Episode (> 85%) | Positive | Validates duration fit and long-form affinity. | |
| Add to Queue | Positive | Increments immediate intent score. | |
| Early Skip (< 30s) | Negative | Penalizes show familiarity and prompt-seeking features. | |
| Dismiss Card | Negative | Lowers exploration bonus for similar candidates. | |
| Remove from Queue | Negative | Prunes short-term weight on similar topics. |
The covariance matrix
-
New User Onboarding:
During cold-start (fewer than 15 recorded interactions), the algorithm suppresses the bandit model weight and relies on curated daypart charts, popular editorial feeds, and selected onboarding interests. -
Linear Blending Transition:
Between 15 and 50 recorded outcomes, the system linearly blends editorial chart ordering with personalized bandit rankings ($w_{\text{bandit}} = \frac{N - 15}{35}$ ). Above 50 interactions, full on-device ranking takes precedence. -
Diversity Constraints:
To prevent single-show dominance, every shelf applies strict diversity deduplication: no single podcast may occupy more than 2 slots on the same horizontal rail.
- Features Overview
- Subscription Folders
- Custom Tags & Icons
- Podcasting 2.0 & Transcripts
- Playback Engine
- Smart Queue & Downloads
- Backup & Restore
boxlore · Local-First Podcast Platform