Skip to content

Recommendation and Ranking

Aswin C edited this page Sep 9, 2026 · 2 revisions

Recommendation & Ranking

boxlore incorporates an on-device personalization system that tailors podcast discovery and automated queue generation to individual listening habits.


Architectural Philosophy

The personalization system is structured with a clear division of responsibility between candidate retrieval and on-device scoring:

  1. Stateless Candidate API:
    External API endpoints serve candidate pools based on public charts, categories, daypart curation, and vector similarity without server-side personalization.
  2. 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.

The Recommendation Pipeline

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
Loading

Mathematical Formulation: LinUCB Contextual Bandit

The ranking engine formulates episode selection as a contextual multi-armed bandit using the LinUCB (Linear Upper Confidence Bound) algorithm.

Feature Vector Formulation

For each candidate episode, a standardized $d$-dimensional feature vector $x \in \mathbb{R}^d$ is synthesized:

  • 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.

Scoring & Exploration Balance

For a candidate represented by context vector $x$, the predicted score is calculated as:

$$\text{Score}(x) = \theta^T x + \alpha \sqrt{x^T A^{-1} x}$$

  • $\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.

Taste Meters & Exponential Decay

To prevent early listening habits from permanently biasing recommendations, boxlore maintains continuous taste meters across three categorical dimensions:

  1. SHOW
  2. GENRE
  3. PUBLISHER

Each taste facet tracks an affinity score $S \in [-1.0, 1.0]$. Every time a new session begins, inactive facets undergo exponential decay toward neutral:

$$S(t + \Delta t) = S(t) \cdot \exp\left(-\frac{\Delta t}{\lambda_{\text{half-life}}}\right)$$

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.


Reward Signal Attribution

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 $+0.50$ Updates model weights; reinforces show and genre taste meters.
Like Episode Positive $+0.80$ High positive boost to genre and creator affinity.
Subscribe to Show Positive $+1.00$ Maximal positive reinforcement for the podcast and category.
Complete Episode (> 85%) Positive $+0.90$ Validates duration fit and long-form affinity.
Add to Queue Positive $+0.40$ Increments immediate intent score.
Early Skip (< 30s) Negative $-0.60$ Penalizes show familiarity and prompt-seeking features.
Dismiss Card Negative $-0.30$ Lowers exploration bonus for similar candidates.
Remove from Queue Negative $-0.40$ Prunes short-term weight on similar topics.

The covariance matrix $A$ and vector $b$ are updated incrementally online via Rank-1 Sherman-Morrison updates:

$$A \leftarrow A + x x^T$$ $$b \leftarrow b + r x$$


Cold-Start Handling & Surface Guardrails

  1. 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.
  2. 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.
  3. 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.