FSRS Spaced Repetition System

Overview

Immersive uses the FSRS (Free Spaced Repetition Scheduler) algorithm for flashcard scheduling. FSRS is a modern spaced repetition algorithm used by Anki 23.10+ that is 20-30% more efficient than the traditional SM-2 algorithm, meaning users need fewer reviews to maintain the same retention rate.

A custom FSRS implementation was built in lib/fsrs/ because the fsrs gem requires ActiveSupport ~> 7.0, which is incompatible with Rails 8.1.

Architecture

Core Algorithm (lib/fsrs/)

File Purpose
lib/fsrs/card.rb FSRS card state representation
lib/fsrs/state.rb Card states: NEW, LEARNING, REVIEW, RELEARNING
lib/fsrs/rating.rb Rating constants: AGAIN (1), HARD (2), GOOD (3), EASY (4)
lib/fsrs/scheduler.rb Core FSRS scheduling algorithm
lib/fsrs/parameters.rb Default FSRS-5 parameters (19 ML-optimized values)
lib/fsrs/scheduling_info.rb Scheduling data structures

Service Layer

  • Reviews::FsrsScheduler (app/services/reviews/fsrs_scheduler.rb) – Wraps the FSRS algorithm for use with Rails Card models. Maps 1-4 ratings to FSRS constants, calculates scheduling, and returns a hash of updates for the Card model.

  • Reviews::AnswerGrader (app/services/reviews/answer_grader.rb) – Auto-grades written and listened answers using Levenshtein distance. Thresholds: 95%+ = Easy, 85-95% = Good, 70-85% = Hard, <70% = Again. Supports user override of auto-grades.

Controller

Reviews::SessionsController (app/controllers/reviews/sessions_controller.rb):

  • show – Displays next due card, checks daily limits, shows interval preview
  • submit_review – Records review with auto-grading, allows user override
  • stats – Returns deck statistics (due cards, retention rate)

Routes

# config/routes/decks.rb
get  "decks/:id/review(/:mode)", to: "reviews/sessions#show"
post "decks/:id/review/submit",  to: "reviews/sessions#submit_review"
get  "decks/:id/review/stats",   to: "reviews/sessions#stats"

Card States and Scheduling

Cards move through states based on review grades:

NEW --> LEARNING --> REVIEW <--> RELEARNING
                      ^              |
                      +--------------+
  • NEW: Not yet reviewed. First review moves to LEARNING or REVIEW depending on grade.
  • LEARNING: Recently introduced. Short review intervals (1min, 10min).
  • REVIEW: Graduated cards with longer intervals based on stability.
  • RELEARNING: Forgotten cards return here before moving back to REVIEW.

FSRS tracks two key metrics per card:

  • Stability: Memory strength (how long until next optimal review)
  • Difficulty: How hard the card is for this user (adjusts scheduling)

Review Modes

Mode Input Grading
Read Self-assessment User selects Again/Hard/Good/Easy
Write Type answer in target language Auto-graded via Levenshtein distance
Listen Type what you hear from audio Auto-graded via Levenshtein distance
Speak Record audio and type transcript Auto-graded via transcript (pronunciation API placeholder)

All auto-graded modes support user override.

Configuration

config/fsrs_scheduler.yml:

default: &default
  desired_retention: 0.9      # 90% retention target
  learning_steps: [1, 10]     # 1min, 10min for new cards
  relearning_steps: [10]      # 10min for forgotten cards
  new_cards_per_day: 20       # Per-deck default
  max_reviews_per_day: null   # Unlimited by default

Per-deck overrides are supported via new_cards_per_day and max_reviews_per_day columns on the decks table.

Database Schema

Cards table (FSRS fields)

  • due_at (datetime) – When the card is next due
  • status (string, default: “new”) – Card state
  • stability (float) – Memory strength
  • difficulty (float) – Card difficulty for this user
  • elapsed_days (integer) – Days since last review
  • scheduled_days (integer) – Scheduled interval
  • reps (integer) – Total review count
  • lapses (integer) – Times forgotten
  • last_review_at (datetime)
  • deck_path (string) – Materialized path for nested decks

Reviews table (FSRS fields)

  • mode (string) – read, write, listen, speak
  • user_answer (text)
  • auto_graded (boolean)
  • user_override_grade (boolean)
  • stability_before / stability_after (float)
  • difficulty_before / difficulty_after (float)
  • pronunciation_score (float) – Placeholder for future pronunciation API
  • phoneme_scores (jsonb) – Placeholder for phoneme-level scores

Decks table (nesting fields)

  • parent_deck_id (bigint) – Parent deck reference
  • path (string) – Materialized path (e.g., “Spanish::Verbs::Present Tense”)
  • depth (integer)
  • new_cards_per_day (integer, default: 20)
  • max_reviews_per_day (integer, nullable)

Design Decisions

  • Per-card scheduling: One FSRS schedule per card, independent of review mode.
  • Keep existing Card STI: The Resource to Card model is better suited for language learning than Anki’s Note to Card to Template model.
  • Nested decks via materialized path: Efficient querying of deck hierarchies without recursive queries.
  • Custom FSRS implementation: Full control over the algorithm and compatibility with Rails 8.1.
  • Pronunciation API placeholder: Fields exist for phoneme-based scoring (Azure Cognitive Services recommended) but currently uses Levenshtein distance as fallback.

Testing

bin/rails test test/services/reviews/
bin/rails test test/models/decks/card_test.rb
bin/rails test test/controllers/reviews/

Future Enhancements

  • Pronunciation assessment API integration (Azure Cognitive Services)
  • Per-user FSRS parameter optimization (after 1000+ reviews)
  • Review analytics dashboard and heatmap
  • Streak tracking and leech detection

This site uses Just the Docs, a documentation theme for Jekyll.