FSRS-плагин для Obsidian: SQL-подобные запросы, Rust/WASM, производительность
Инструмент интервального повторения заметок Obsidian должен использовать современный алгоритм, работать локально с заметками как есть (без переписывания в карточки).
Существующие в Obsidian плагины останавливаются на алгоритме SM-2 образца 1987 года.
Альтернативные решения есть «где-то еще», вне свободного ПО, вне Markdown‑first архитектуры — привязаны к облаку или проприетарному формату.
Я написал свой, потому что не нашёл подходящего.
FSRS, вычислительное ядро на Rust, скомпилированное в WebAssembly, и SQL‑подобный синтаксис для табличной выборки.
В статье — архитектура с WebAssembly, собственный парсер, лексер, замеры производительности. Любые запросы обрабатываются в сотых долях секунды. Blazingly fast ?
Это техническая статья. Если хотите пошаговое руководство для пользователя — вот обзорная статья.

Зачем четвёртый плагин для повторений?
На момент написания в Obsidian есть три популярных решения: obsidian-spaced-repetition, obsidian-recall и obsidian-review. Все используют SM-2 — алгоритм которому скоро 40 лет. Он работает, но требует примерно на 30% больше повторений чем FSRS при том же уровне запоминания.
Главные недостатки SM-2:
Одинаковый интервал для материала любой сложности
Не обрабатывает пропуски — «сброс» прогресса после перерыва
Нет понятия извлекаемости (retrievability) — вероятности вспомнить карточку сейчас
FSRS это шаг вперёд. Но его нет в Obsidian. Точнее — не было.

Как устроен FSRS в двух словах
FSRS оперирует DSR-моделью из трёх параметров:
Difficulty (сложность) — насколько труден материал. Диапазон: 0–10
Stability (стабильность) — прочность запоминания в днях
Retrievability (извлекаемость) — вероятность вспомнить карточку прямо сейчас
После каждого ответа (Again / Hard / Good / Easy) алгоритм пересчитывает сложность и стабильность. Извлекаемость меняется непрерывно.
Алгоритм использует 21 параметр, подобранный машинным обучением на миллионах реальных повторений.

Архитектура: почему Rust и WebAssembly
Плагин Obsidian — это JavaScript. Но FSRS требует точных вычислений с плавающей точкой на каждом повторении.
Я выбрал Rust по трём причинам:
Производительность — WASM работает на порядок быстрее JS на численных вычислениях
Экосистема — крейт
rs-fsrsот сообщества open-spaced-repetition с эталонной реализацией FSRSБезопасность типов — в Rust невозможно перепутать
difficultyиstability
Разделение ответственности:
TypeScript Rust/WASM ───────── ───────── • Obsidian API • FSRS-вычисления • UI / рендеринг • Парсинг SQL-подобного синтаксиса • Файловая система • Парсинг YAML/JSON • Жизненный цикл плагина • Фильтрация и сортировка • Кнопки, модалки • Кэш карточек

TypeScript — тонкая обвязка над Obsidian API. Вся логика — в WASM.
Производительность: граница WASM ⇆ JS
Минимизация копирования через границу
Кэш внутри WASM — фильтрация и сортировка выполняются там же, в Rust. Через границу передаётся только результат (20–200 строк), а не все 10 000.
Инкрементальные обновления — при ответе на карточку пересчитывается только одна запись.
metadataCache: не читать файлы
Главный выигрыш в скорости — отказ от чтения файлов. Obsidian хранит распарсенный frontmatter в metadataCache — внутреннем кэше, который обновляется при каждом изменении заметки.
Плагин проверяет наличие FSRS-полей через metadataCache.getFileCache() — это мгновенный доступ в памяти, без I/O. Из 105 607 файлов реально читаются только те, где frontmatter уже содержит reviews.
Для проверки: собственный парсер плагина обрабатывает 105k файлов за 16 секунд, 100к отфильтровывает, а 5к обрабатывает.
Obsidian на индексацию 105к тратит ~20 минут, а на свежедобавленные 5000 карточек, ~120 с. Так что корректно сравнивать 16 секунд плагина и 120 Обсидиана. Но он всё равно это делает — так что эффективнее брать готовое.
Циферки
FSRS-расчёт для всех карточек выполняется один раз при первом сканировании хранилища. После этого кэш живёт в WASM — все последующие операции (загрузка таблицы, тепловая карта, обновление одной карточки) работают с уже готовыми данными и не зависят от объёма.
Операция |
Большое хранилище (105k файлов, ~5000 карточек) |
Маленькое хранилище (710 файлов, 104 карточки) |
|---|---|---|
Первичное сканирование (FSRS для всех карточек) |
3.2 с |
0.04 с |
Загрузка таблицы (после кэша) |
0.07 с |
0.04 с |
Тепловая карта |
0.02 с |
0.01 с |
Обновление одной карточки |
< 0.01 с |
< 0.01 с |
Разница между 5000 и 100 карточек после кэширования — 0.03 с. Логи болшое, Логи малое с плагинами
Любое действие плагина, после первичного расчёта, выполняется в сотые доли секунды.
Узкое место
3.2 секунды на первичную загрузку — это FSRS-расчёт для каждой из 5 000 карточек. Выполняется однократно.
Можно было бы сохранять состояния в постоянный кэш на диск, но:
Сложность синхронизации между устройствами (кэш на диске может устареть)
5 000 карточек / 3.2 секунды — приемлемо для реального использования
После первого запуска последующие открытия плагина работают мгновенно — кэш уже в WASM
Что не так (о компромиссах)
LIMIT в текущей реализации не прерывает обработку — чтобы гарантированно получить первые N строк по извлекаемости, всё равно нужно оценить все карточки.
Компромисс принят сознательно: размер хранилища реального пользователя редко превышает 5000–10000 записей, полный обход + сортировка занимают 0.005–0.010 с.
Кэш в WASM вместо локального стейта
Весь кэш (HashMap<filePath, CachedCard>) живёт внутри WASM как глобальная переменная. Плагин не хранит состояния в TypeScript вообще.
Почему:
Единый источник истины — нет рассинхрона между JS-стейтом и WASM-вычислениями
Быстрые запросы — фильтрация/сортировка идут там же где данные
Инкрементальное обновление — точечные команды «обнови эту карточку» / «удали эту»
Где живут данные. Прогресс повторений хранится прямо в YAML-frontmatter заметки:
--- reviews: - date: "2026-05-03T12:00:00Z" rating: 2 - date: "2026-05-04T08:30:00Z" rating: 3 ---
due, stability, difficulty и state не хранятся — WASM-ядро вычисляет их на лету из истории.
SQL-подобный язык для таблиц
Главная фича плагина — выборка на повторение. Это блок fsrs-table. Вы пишете в markdown-заметке:
```fsrs-table SELECT file as " ", d as "D", s as "S", r as "R", date_format(due, '%d.%m.%Y') as "Next" LIMIT 20 ```
И получаете живую таблицу с карточками, которая автообновляется при повторениях.

Реализован полноценный парсер с нуля — лексер → парсер → AST → evaluator:
Лексер — разбивает строку запроса на токены
Парсер — строит синтаксическое дерево с учётом приоритета операторов
Evaluator — обходит AST и вычисляет условие для каждой карточки
Поддерживается:
SELECT— выбор полей и переименование черезASWHERE— условия со сравнениями (=,!=,<,>,<=,>=) и логическими операторами (AND,OR)ORDER BY— сортировка по возрастанию/убываниюLIMIT— ограничение количества строкdate_format()— форматирование дат
Для difficulty, stability и retrievability доступны однобуквенные псевдонимы d, s, r — сокращения, принятые в сообществе FSRS.
Чего fsrs-table не умеет:
Вложенные запросы,
JOIN, агрегации (COUNT,SUM…) — нетПорядок колонок в
SELECT— можно менять как угодно ✅Несуществующее поле в
WHEREилиSELECT— ошибка с указаниемLIMITограничивает вывод, но не прерывает обработку (подробнее выше)Только чтение.
INSERT,UPDATE,DELETEчерез SQL нельзя
Отказ от моков как следствие архитектуры
Моки не нужны не потому что «запрещены», а потому что нечего мокать.
TypeScript в плагине — тонкая обёртка над Obsidian API. Вся логика — в Rust/WASM. Мокать Obsidian значит проверить, закроет ли плагин <div>, или вызвал ли vault.read() — тривиальный клей, не стоящий тестов. Стоит проверять другое: связку своего TypeScript со своим же WASM.
Поэтому тесты делятся на два уровня:
Rust-тесты (184 штуки) — чистые функции, изолированные от окружения
TypeScript-тесты (86 штук) — unit-тесты чистых функций и интеграционные TS → WASM
Интеграционные тесты начинаются и заканчиваются на собственном коде: сырая строка, параметр либо вызов на входе (TS) → парсинг (WASM) → запрос к WASM-кэшу (WASM) → результат (TS).

CI/CD: сборка, тесты, релиз одной кнопкой
Пайплайн в GitLab CI из семи стадий:
check—cargo fmt,cargo clippy,cargo testbuild-wasm—wasm-pack buildencode-wasm— встраивание WASM в base64 для бандлаtest— TypeScript-тесты (vitest)lint—tsc --noEmit+ ESLintbuild— финальная сборкаmain.jsrelease— автоматический релиз на GitHub

Текущее состояние
Плагин готов к использованию.
Протестирован на Ubuntu, Windows и на Android.
Плагин доступен в каталоге сообщества Obsidian.
Что уже готово:
Фильтрация и сортировка через SQL-подобный синтаксис
Тепловая карта повторений (Heatmap)
Локализация на русский, английский и китайский
CI/CD, который сам собирает и публикует релизы
Прозрачное хранение — все данные в YAML-frontmatter ваших
.md-файловИнтеграционные тесты TS → WASM (сырой SQL, без моков)
Тепловая карта
Что планируется:
Собрать обратную связь
Доработать интерфейс для мобилок
Возможно, расширить SQL-синтаксис
Как установить
Плагин доступен в каталоге сообщества Obsidian.
Settings → Community plugins → Browse
Найдите FSRS → Install
Включите плагин в Settings → Community plugins
Также можно установить через BRAT для самых свежих сборок:
Установи BRAT
Settings → BRAT → Add Beta plugin
Вставь:
https://github.com/Evgene-Kopylov/fsrs_plugin
Стек
TypeScript — Obsidian API, UI
Rust — вычислительное ядро (WASM)
esbuild — сборка JS-бандла
wasm-pack — сборка WASM
Vitest — тесты TypeScript
GitLab CI/CD — пайплайн
Ссылки
Копылов Е.В., 2026