FSRS-плагин для Obsidian: SQL-подобные запросы к карточкам, Rust/WASM

Страницы:  1

Ответить
 

Professor Seleznov


FSRS-плагин для Obsidian: SQL-подобные запросы, Rust/WASM, производительность
Инструмент интервального повторения заметок Obsidian должен использовать современный алгоритм, работать локально с заметками как есть (без переписывания в карточки).
Существующие в Obsidian плагины останавливаются на алгоритме SM-2 образца 1987 года.
Альтернативные решения есть «где-то еще», вне свободного ПО, вне Markdown‑first архитектуры — привязаны к облаку или проприетарному формату.
Я написал свой, потому что не нашёл подходящего.
FSRS, вычислительное ядро на Rust, скомпилированное в WebAssembly, и SQL‑подобный синтаксис для табличной выборки.
В статье — архитектура с WebAssembly, собственный парсер, лексер, замеры производительности. Любые запросы обрабатываются в сотых долях секунды. Blazingly fast 🦀
Это техническая статья. Если хотите пошаговое руководство для пользователя — вот обзорная статья.
pic
демонстрация плагина: таблица карточек и всплывающий предпросмотр
-
Зачем четвёртый плагин для повторений?
На момент написания в Obsidian есть три популярных решения: obsidian-spaced-repetition, obsidian-recall и obsidian-review. Все используют SM-2 — алгоритм которому скоро 40 лет. Он работает, но требует примерно на 30% больше повторений чем FSRS при том же уровне запоминания.
Главные недостатки SM-2:
  • Одинаковый интервал для материала любой сложности
  • Не обрабатывает пропуски — «сброс» прогресса после перерыва
  • Нет понятия извлекаемости (retrievability) — вероятности вспомнить карточку сейчас
FSRS это шаг вперёд. Но его нет в Obsidian. Точнее — не было.
pic
sm-2-vs-fsrs
-
Как устроен FSRS в двух словах
FSRS оперирует DSR-моделью из трёх параметров:
  • Difficulty (сложность) — насколько труден материал. Диапазон: 0–10
  • Stability (стабильность) — прочность запоминания в днях
  • Retrievability (извлекаемость) — вероятность вспомнить карточку прямо сейчас
После каждого ответа (Again / Hard / Good / Easy) алгоритм пересчитывает сложность и стабильность. Извлекаемость меняется непрерывно.
Алгоритм использует 21 параметр, подобранный машинным обучением на миллионах реальных повторений.
pic
DSR-schema
-
Архитектура: почему 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
• Жизненный цикл плагина • Фильтрация и сортировка
• Кнопки, модалки • Кэш карточек
pic
fsrs-plugin-schema
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
```
И получаете живую таблицу с карточками, которая автообновляется при повторениях.
pic
отрендеренная таблица fsrs-table с карточками
Реализован полноценный парсер с нуля — лексер → парсер → AST → evaluator:
  • Лексер — разбивает строку запроса на токены
  • Парсер — строит синтаксическое дерево с учётом приоритета операторов
  • Evaluator — обходит AST и вычисляет условие для каждой карточки
Поддерживается:
  • SELECT — выбор полей и переименование через AS
  • WHERE — условия со сравнениями (=, !=, <, >, <=, >=) и логическими операторами (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).
pic
tests-terminal
-
CI/CD: сборка, тесты, релиз одной кнопкой
Пайплайн в GitLab CI из семи стадий:
  • checkcargo fmt, cargo clippy, cargo test
  • build-wasmwasm-pack build
  • encode-wasm — встраивание WASM в base64 для бандла
  • test — TypeScript-тесты (vitest)
  • linttsc --noEmit + ESLint
  • build — финальная сборка main.js
  • release — автоматический релиз на GitHub
pic
ci-green
-
Текущее состояние
Плагин готов к использованию.
Протестирован на Ubuntu, Windows и на Android.
Плагин доступен в каталоге сообщества Obsidian.
Что уже готово:
  • Фильтрация и сортировка через SQL-подобный синтаксис
  • Тепловая карта повторений (Heatmap)
  • Локализация на русский, английский и китайский
  • CI/CD, который сам собирает и публикует релизы
  • Прозрачное хранение — все данные в YAML-frontmatter ваших .md-файлов
  • Интеграционные тесты TS → WASM (сырой SQL, без моков)
  • Тепловая карта
Что планируется:
  • Собрать обратную связь
  • Доработать интерфейс для мобилок
  • Возможно, расширить SQL-синтаксис

-
Как установить
Плагин доступен в каталоге сообщества Obsidian.
  • Settings → Community plugins → Browse
  • Найдите FSRSInstall
  • Включите плагин в Settings → Community plugins
Также можно установить через BRAT для самых свежих сборок:
-
Стек
  • TypeScript — Obsidian API, UI
  • Rust — вычислительное ядро (WASM)
  • esbuild — сборка JS-бандла
  • wasm-pack — сборка WASM
  • Vitest — тесты TypeScript
  • GitLab CI/CD — пайплайн

-
Ссылки Е. Копылов, 2026-Источник
 
Loading...
Error