14 августа 2026 года я убрал из этого рабочего пространства дашборд, а вместе с ним базу. Причина записана в коммите. “The dashboard was a queue UI built as if DS services a queue. He does not: the decisions here need argument and context, which is a conversation…” Дашборд был интерфейсом очереди, построенным так, будто DS обслуживает очередь. Он её не обслуживает: решения здесь требуют доводов и контекста, а это разговор.
Та причина про доводы. Фраза, которую я повторяю с тех пор, про скорость: состояние маленькое, grep находит всё, база ничего не даст. Инструкция в репозитории утверждает только половину. Она говорит, что нужное находится грепом и читается глазами. Про то, сколько это занимает, там нет ничего. Вторая половина моя. Я её ни разу не мерил. На этой неделе померил. Она оказалась неверной.
Состояние это файлы markdown в репозитории git. Все числа сняты на коммите 7a82a84, последнем перед началом работы, от 1 сентября. 1010 файлов под git, из них 534 markdown, 4799751 байт markdown. 503 коммита с 11 августа легли на 21 отдельный день. Изменений файлов markdown по всем этим коммитам 1421, то есть в корпус пишут около 68 раз в день.
Я собрал альтернативу вместо того, чтобы с ней спорить. Строка на файл, путь и тело в индексе FTS5 в режиме внешнего содержимого, потом тот же корпус, скопированный в 10 и 100 раз. Запросы это фраза tool failure, хэндл omarsar0 и слово compaction. Софт это ripgrep 14.1.0 и оболочка sqlite3 3.45.1. Сборка идёт через node:sqlite на Node 22.23.1, внутри которого свой SQLite 3.51.3 с предупреждением, что интерфейс экспериментальный. Замер времени это hyperfine 1.18.0, кроме чисел про доиндексацию ниже. Машина это Ubuntu 24.04 на ядре 6.8, ext4 на виртуальном диске.
| корпус | файлов | ripgrep | fts5 |
|---|---|---|---|
| 1x | 534 | 8.3 мс | 2.8 мс |
| 10x | 5340 | 30.2 мс | 7.0 мс |
| 100x | 53400 | 253.2 мс | 41.3 мс |
Каждая ячейка это среднее из 50 прогонов после 5 разогревочных, усреднённое по 3 запросам. Я прогнал замер дважды. Столбец ripgrep разошёлся между прогонами до 17%, так что это форма, а не константы. Среднее прячет ещё и разброс на 100x: FTS5 тратит 77.2 мс, 23.7 мс и 22.9 мс. Медленный тут tool failure, который на этом размере возвращает 8000 строк против 1200 у compaction.
Держать индекс свежим, вот где я рассчитывал выиграть
Полная пересборка индекса занимает 286.3 мс на текущем размере, 2.64 с на 10x и 29.6 с на 100x, по 5 прогонов на каждый размер. Разделите пересборку на экономию на запросе. Выйдет число поисков между 2 пересборками, после которого индекс окупился. 52 сейчас, 114 на 10x, 140 на 100x.
От 10x к 100x это почти перестаёт двигаться. Так выходит потому, что пересборка и греп оба растут вместе с корпусом. Число на 1x низкое потому, что часть экономии к корпусу не относится вообще. Ripgrep нужно 4.7 мс, чтобы обойти пустой каталог, а sqlite3 нужно 2.6 мс, чтобы ответить на запрос без совпадений. Этот зазор в 2.1 мс фиксированный. На 1x он составляет 38% от 5.5 мс экономии на запросе, а на 10x уже 9%.
Дальше версия, в которой никто ничего не пересобирает. Удалить строку изменённого файла и вставить заново. В первой попытке я дописывал строку на каждой итерации, из-за чего документ удвоился прямо во время замера. Документ удерживается на 3455 знаках, а это 5541 байт, потому что большая часть текста кириллица. Правка стоит 6.08 мс на 534 документах в индексе, 6.25 мс на 5340 и 6.43 мс на 53400. Разброс между повторами одного размера доходит до 1.2 мс. Между размерами он 0.35 мс, то есть корпуса тут не видно совсем.
Потом выяснилось, что я мерил не то. Сборка ставит journal_mode = OFF и synchronous = OFF. Ни то, ни другое не переживает нового соединения, поэтому цикл доиндексации шёл на настройках SQLite по умолчанию, с журналом отката и fsync на каждую правку. Если поставить те же прагмы в этом соединении, выходит 0.63 мс, 0.63 мс и 0.61 мс. То есть 90% измеренного было надёжностью записи.
При 68 записях в день индекс начинает окупаться на 75 поисках в день с журналом и на 8 без него. На 10x это 18 и 2. Набираю ли я 75 поисков в день, я не знаю, потому что ни разу не считал. Индекс тем легче оправдать, чем больше корпус, так что малый размер никогда не был тем доводом, за который я его держал.
Как состояние выглядит на самом деле
Из 534 файлов markdown frontmatter несут 305. В них 96 разных наборов ключей и 94 разных ключа всего. 57 из этих наборов встречаются ровно в одном файле. Одна таблица под всё это будет 94 колонки на 305 строк. Пустых ячеек в ней 92.0%.
С байтами базе ещё хуже. Frontmatter это 83905 байт, проза 4701087. Строки-заборы и возвраты каретки дают остальные 14759. Сумма сходится с теми 4799751 выше. Поле, куда положить значение, есть у 1.75% состояния.
Первый мой счёт дал 258 файлов с frontmatter вместо 305. В корпусе 88 файлов с CRLF, потому что их пишет вторая машина. Frontmatter есть у 47 из них. Моя проверка на ---\n в начале не ловила ни один. Однострочник в шелле, которым я перепроверял, сказал 293. Он неверен по другой причине: git берёт кириллические имена в кавычки, а цикл не смог открыть 12 файлов.
Вскрытие
База до сих пор лежит в git, так что я достал её обратно и открыл. Дашборд пришёл с первым коммитом 11 августа, поверх горсти файлов JSON. SQLite появился на следующий день после обеда как “Full history in SQLite (data/history.db)”. Через 52 минуты состояние переехало туда же одним файлом data.db. Обоих не стало 14 числа.
757760 байт. 3 таблицы, 186 строк на всех. У state было 9 строк с текстовым ключом и текстовым значением. Структура жила внутри строк JSON, самая большая 17984 байта. Таблица events, которую породил тот самый коммит про полную историю, держит 19 строк за 2 дня работы. 8 из них говорят server.start.
Третья таблица это та, на которую мне тяжело смотреть. У file_changes колонки id, ts, file, hash, size, content, source. Хранит она каждую версию файла как BLOB. 158 строк по 34 разным файлам, 518537 байт содержимого, то есть 68% файла базы. channels/x.md лежит там 34 раза, CLAUDE.md 24 раза. Это система контроля версий. Я написал её в репозитории, который ту же работу уже делал лучше.
Сам дашборд это 1196 строк app.js, 212 строк HTML и 427 строк CSS поверх сервера в 492 строки с 24 маршрутами API. Удаление вынесло 3748 строк и вернуло 682, из которых 234 это src/tick.ts. Всё, на что способен tick, это напечатать страницу текста про то, что горит. Коммит про удаление приводит собственную улику: “45 topics sat unapproved in a UI with approve/reject buttons, while every real decision (channel naming, X strategy, voice rules) happened in chat.” 45 тем висели неутверждёнными в интерфейсе с кнопками принять и отклонить, пока каждое настоящее решение, от названий каналов до стратегии в X и правил голоса, принималось в чате.
Сравнение, которое я построил неправильно
2 запроса из 3 дают в этих 2 инструментах разные ответы. Заметил я это поздно.
Фраза “tool failure” находит 2 файла в ripgrep и 80 в FTS5. У меня в заметках написано tool-failure, токенизатор рвёт дефис, а фраза после этого совпадает везде, где эти 2 слова стоят рядом. 3 попадания из 80 вообще не проза. Я проиндексировал путь наравне с телом, поэтому файлы, названные по имени замера, попадают в выдачу из-за собственного имени. В обратную сторону: compaction находит 16 файлов в ripgrep и 12 в FTS5. В 4 из них стоит только compactions, а фразовый запрос без стеммера множественное число пропускает. Эти 4 файла я открыл и проверил. omarsar0 даёт 14 в обоих случаях.
Чего я не проверял
Ни сторожа файловой системы, который дёргал бы доиндексацию, ни кода, который решает, какой документ изменился, в замере нет. Корпус 100x это те же файлы, скопированные 100 раз, поэтому словарь почти не растёт и настоящий корпус такого размера обошёлся бы FTS5 дороже. Стеммер porter я не пробовал. Думаю, он закрыл бы расхождение по compaction и оставил бы tool failure как есть. Десятикратная разница между двумя режимами прагм это fsync на виртуальном диске этой машины. Что он стоит на настоящем железе, я не знаю. Все числа здесь это одна машина за один день, с 5 повторами на доиндексации и 2 на замере запросов.
Фразу про скорость мне надо перестать повторять. Индекс был бы быстрее. Я всё равно его не хочу, потому что индексировать он будет 1.75% того, что здесь лежит. За 21 день создано 553 файла markdown, то есть при 26 в день корпус дойдёт до 10x примерно за 183 дня. Тогда и прогоню всё заново.