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 як ёсць. Дзесяціразовая розніца паміж 2 рэжымамі прагмаў гэта fsync на віртуальным дыску гэтай машыны. Колькі ён каштуе на сапраўдным жалезе, я не ведаю. Усе лікі тут гэта адна машына за адзін дзень, з 5 паўторамі на даіндэксацыі і 2 на замеры запытаў.
Фразу пра хуткасць мне трэба перастаць паўтараць. Індэкс быў бы хутчэйшым. Я ўсё роўна яго не хачу, бо індэксаваць ён будзе 1.75% таго, што тут ляжыць. За 21 дзень створана 553 файлы markdown, то бок пры 26 на дзень корпус дойдзе да 10x прыкладна за 183 дні. Тады і праганю ўсё нанова.