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 previewsubmit_review– Records review with auto-grading, allows user overridestats– 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 duestatus(string, default: “new”) – Card statestability(float) – Memory strengthdifficulty(float) – Card difficulty for this userelapsed_days(integer) – Days since last reviewscheduled_days(integer) – Scheduled intervalreps(integer) – Total review countlapses(integer) – Times forgottenlast_review_at(datetime)deck_path(string) – Materialized path for nested decks
Reviews table (FSRS fields)
mode(string) – read, write, listen, speakuser_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 APIphoneme_scores(jsonb) – Placeholder for phoneme-level scores
Decks table (nesting fields)
parent_deck_id(bigint) – Parent deck referencepath(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