ActiveRecall
ActiveRecall is a spaced-repetition system that allows you to treat arbitrary ActiveRecord models as if they were flashcards to be learned and reviewed. It is based on, and is intended to be backwards compatible with, the okubo gem. The primary differentiating features are that it lets the user specify the scheduling algorithm and is fully compatible with (and requires) Rails 6+ and Ruby 3+.
Installation
Add this line to your application's Gemfile:
gem 'active_recall'
And then execute:
$ bundle
$ rails generate active_recall
$ rails db:migrate
Or, if you were using the Okubo gem and want to migrate your data over, execute:
$ bundle
$ rails generate active_recall --migrate_data true
$ rails db:migrate
Or install it yourself as:
$ gem install active_recall
The generator creates all the migrations any algorithm needs (including the easiness_factor column for SM2 and the FSRS-specific columns), so you don't have to revisit migrations when you switch algorithms later.
Quick Start
The fastest way to get going — no algorithm choice, no grade scale, just right/wrong feedback. This uses the default LeitnerSystem.
Suppose you have an application allowing your users to study words in a foreign language. Use has_deck to set up a deck of flashcards:
class Word < ActiveRecord::Base
end
class User < ActiveRecord::Base
has_deck :words
end
user = User.create!(name: "Robert")
word = Word.create!(kanji: "日本語", kana: "にほんご", translation: "Japanese language")
user.words << word
user.words.untested #=> [word]
user.right_answer_for!(word)
user.words.known #=> [word]
user.wrong_answer_for!(word)
user.words.failed #=> [word]
That's it. Want graded feedback (Again/Hard/Good/Easy) or modern scheduling? See Choosing an Algorithm below.
Choosing an Algorithm
Not sure which to pick? Stick with the default
LeitnerSystem— it works out of the box and only needs right/wrong feedback. Reach forFSRSwhen you want modern, evidence-based scheduling and are willing to collect 1–4 ratings ("Again / Hard / Good / Easy") from users.
The full menu, in increasing order of sophistication:
| Algorithm | Type | How you grade | Reach for it when |
|---|---|---|---|
LeitnerSystem (default — start here) |
binary | right_answer_for! / wrong_answer_for! |
You want the simplest thing that works |
SoftLeitnerSystem |
binary | right_answer_for! / wrong_answer_for! |
Leitner is too punishing on occasional lapses |
FibonacciSequence |
binary | right_answer_for! / wrong_answer_for! |
You want faster-growing intervals than Leitner |
SM2 |
gradable | score!(0..5, item) |
You want the classic SuperMemo behavior users know from Anki |
FSRS (modern recommendation) |
gradable | score!(1..4, item) |
You're building something serious and want best-in-class retention |
Binary algorithms expect right-or-wrong feedback (user.right_answer_for!(item) / user.wrong_answer_for!(item)). Gradable algorithms expect a numeric grade per review (user.score!(grade, item)). Mixing them — e.g. calling right_answer_for! while configured to use SM2 — raises ActiveRecall::IncompatibleAlgorithmError.
Configuration
Skip this section if you're sticking with the default LeitnerSystem — there's nothing to configure.
To switch algorithms, set algorithm_class from a Rails initializer file:
# config/initializers/active_recall.rb
ActiveRecall.configure do |config|
config.algorithm_class = ActiveRecall::FSRS # or SM2, SoftLeitnerSystem, FibonacciSequence
end
FSRS-specific configuration
FSRS exposes three optional knobs. All have sensible defaults; tune only if you have a reason to:
fsrs_request_retention— target retention probability (default0.9). Lower → longer intervals, more forgetting tolerated.fsrs_maximum_interval— caps the scheduled interval, in days.fsrs_weights— array of FSRS weights for advanced tuning.
ActiveRecall.configure do |config|
config.algorithm_class = ActiveRecall::FSRS
config.fsrs_request_retention = 0.85
config.fsrs_maximum_interval = 365
end
Usage with binary algorithms
Applies to LeitnerSystem, SoftLeitnerSystem, and FibonacciSequence.
# Initially adding a word
user.words << word
user.words.untested #=> [word]
# Guessing a word correctly
user.right_answer_for!(word)
user.words.known #=> [word]
# Guessing a word incorrectly
user.wrong_answer_for!(word)
user.words.failed #=> [word]
# Listing all words
user.words #=> [word]
As time passes, words need to be reviewed to keep them fresh in memory:
# Three days later...
user.words.known #=> []
user.words.expired #=> [word]
Guessing a word correctly several times in a row makes the word take longer to expire, demonstrating mastery:
user.right_answer_for!(word)
# One week later...
user.words.expired #=> [word]
user.right_answer_for!(word)
# Two weeks later...
user.words.expired #=> [word]
user.right_answer_for!(word)
# One month later...
user.words.expired #=> [word]
Usage with SM2
SM2 uses a 0–5 grade scale:
| Grade | Meaning |
|---|---|
5 |
Perfect response |
4 |
Correct response after a hesitation |
3 |
Correct response recalled with serious difficulty |
2 |
Incorrect response, but close |
1 |
Incorrect response with familiarity |
0 |
Complete blackout |
Grades ≥ 3 count as a success: the box advances and times_right increments. Grades < 3 reset the box to 0 and increment times_wrong. Each item's easiness_factor starts at 2.5 and is clamped to a minimum of 1.3.
user.words << word
user.score!(5, word) # perfect recall — box advances, EF rises
user.score!(2, word) # incorrect — box resets to 0
Calling user.right_answer_for!(word) while SM2 is configured raises ActiveRecall::IncompatibleAlgorithmError — use score! instead.
Usage with FSRS
FSRS uses a 1–4 grade scale matching the familiar Anki buttons:
| Grade | Meaning |
|---|---|
1 |
Again (lapse) |
2 |
Hard |
3 |
Good |
4 |
Easy |
FSRS tracks stability, difficulty, state, and lapses per item. Those columns are added automatically by rails generate active_recall — no extra setup needed.
user.words << word
user.score!(3, word) # "Good" — typical successful recall
user.score!(1, word) # "Again" — counts as a lapse
Calling user.right_answer_for!(word) while FSRS is configured raises ActiveRecall::IncompatibleAlgorithmError — use score! instead.
Reviewing
In addition to the expired scope, ActiveRecall provides a suggested reviewing sequence for all unknown words in the deck. Words are randomly chosen from untested, failed, and expired items, in that order of precedence. This works the same for every algorithm.
user.words.review #=> [word]
user.right_answer_for!(word)
# ... continuing until all untested, failed, and expired words have been guessed correctly.
user.words.review #=> []
You can also just get the next word to review:
user.words.next #=> word
user.right_answer_for!(word)
# ... continuing until all untested, failed, and expired words have been guessed correctly.
user.words.next #=> nil
Development
After checking out the repo, run bin/setup to install dependencies.
Then, run bin/spec to run the tests.
You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install.
To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/jaysonvirissimo/active_recall.
License
The gem is available as open source under the terms of the MIT License.