История идеи проекта
Сидел я и делал чанкирование разбора конфигурации 1С в PostgreSQL. Выгрузка конфы мне досталась с 43 расширениями и ноль документации по этим расширениям после 5 команд разработки.
Ну, нужно было её разобрать, обогатить бизнес-терминами и превратить во что-то удобоваримое вроде как для RAG. Было всё это долго, нудно, на Питоне, с помощью локальной LLM. Сам я в настоящий момент 1с-ник, но начинал я в своё время с MSSQL-2000 (если кто-то помнит такой), T-SQL, PowerScript, Pascal и пр. романтики.
Сидел я, думал о светлом будущем, когда с помощью RAG локальная моделька будет мне писать скрипты, и даже без ошибок (до некоторой степени это получилось). Ну как-то слово за слово, и мне подумалось: есть же семантический поиск по RAG, реализуется MCP-сервером. По сути это запросы — точные, но довольно медленные. А почему тогда во всех остальных случаях, работая с выгрузкой напрямую, модель раз за разом лезет в переборщики файлов — пусть правильно написанные и крайне оптимизированные, но всё же переборщики? КОТОРЫЕ ТРАТЯТ МАССУ ТОКЕНОВ ТУПО НА ЧТЕНИЕ ДЛЯ ПОИСКА?! Почему нельзя это делать запросами в базу данных — нормальными, дешёвыми, точными?
И вспомнил я заодно старую идею: когда-то файловую систему в Windows (проект WinFS, времён Vista) хотели сделать похожей на БД, чтобы радикально ускорить работу с файлами. Идею потом свернули, и она как-то забылась. НО я не забыл :):)
Идея-то была интересная. Итого — нужно было дать модели инструмент, который заметно экономил бы токены и ускорял поиск данных (особенно в больших массивах кода). И подумалось мне, что не один я такой — есть целый коллектив разработчиков, которому такой инструмент может пригодиться. Поэтому Бог с ним, с Питоном, будем писать на Rust, потому что Rust — это база! (с)
Сейчас была бы длинная история — как я описывал, преодолевал, раздумывал, отлаживал и т.д. Но вот кому это интересно?
Основные моменты:
Обновление базы, и кэша. Зачем базе вообще обновляться? Изначально проект планировался как простая библиотека кода — что-то вроде локальной базы, к которой модель ходит за ответами. Но какой в нём смысл, если та же модель тут же и правит файлы?! Без живого обновления база становится бесполезна сразу после первой правки кода моделью. Поэтому пришлось ловить изменения в реальном времени — отсюда демон с file watcher, отсюда и всё остальное в списке ниже.
Один писатель, много читателей. Демон пишет в SQLite, MCP-серверы только читают. PID-локов нет: к одному .code-index/index.db цепляется сколько угодно процессов параллельно(несколько моделей, говоря обычным языком) — supervisor поднимает их по одному на репо.
На старте демону надо понять, какие файлы изменились с прошлого запуска, чтобы не пересчитывать индекс целиком. Раньше для этого SHA-256 считался по всем файлам — на 93 000 это ~163 с. Теперь сначала проверяются mtime и file_size, и SHA считается только для тех, у кого метаданные сдвинулись. Получается ~4 с.
WAL раздулся до 43 ГБ за сутки. Демон работает долго и постоянно пишет — за сутки на стенде с 13 индексируемыми папками index.db-wal-файлы съели 45 ГБ свободного места. Для одного репо index.db-wal весил 19 ГБ при основной БД в 4.7 ГБ. PRAGMA wal_autocheckpoint переносит страницы из WAL в БД; физический размер файла при этом не меняется. Лечится PRAGMA wal_checkpoint(TRUNCATE) после каждой пачки.
Multi-config 1С: base + 43 расширения. Те самые расширения из вступления. Современная выгрузка 1С — это base + дерево из нескольких десятков extension-папок, каждая со своим Configuration.xml. Расширения переопределяют и добавляют объекты базы. Индекс должен пройти по всему дереву целиком. Парсер рекурсивно (глубина ≤ 2) находит все Configuration.xml и сводит объекты в одну таблицу через INSERT OR IGNORE.
Содержимое файлов в индексе. Сначала индекс был чисто структурный — функции, классы, граф вызовов, без самого текста файлов. Бывают задачи, где агенту нужен именно текст: прочитать файл целиком или прогнать regex по живому исходнику. Под них появилась таблица file_contents со zstd-сжатием (×5 на BSL) и защитой от zstd-bomb на 256 МБ. Затем таблицы под разбор структуры xml-выгрузки 1с. Затем файл связей объектов 1с. В общем, проект все больше уходил в сторону 1с, поэтому я его разделил на непосредственно code-index и bsl-indexer.
Кэш-инвалидация в три эшелона. Перед индексом стоит кэширующий прокси (mcp-cache-ci) — он экономит токены и время на повторных запросах от агентов. Когда файл меняется, ответы, построенные из него, протухают, и нужно точечно их сносить. Прокси при выдаче ответа запоминает, из каких файлов этот ответ собран. Демон после переиндексации шлёт ему POST /invalidate, и кэш сносит зависимые записи. Третий эшелон — TTL: если событие потерялось, запись протухнет сама.
И там еще по мелочи. А, потом уже какая-то сказка началась, с федеративными удаленными репо и пр.
Итого - первоначальная идея соблюдается, модель(даже моделИ!) обращаются и меняют файлы, база это подхватывает моментально, ну в общем - первоначальная идея соблюлась.
Дальше - минусы - пришлось(и иногда приходится:)) модели объяснять, что она работает в проиндексированном проекте, что нечего тут грепать/глобать/ридить и башить(простигосподи:)), что надо использовать правильные инструменты. Несмотря на категорические требования в Claude.md и отдельном файле с правилами, со временем(заполнения контекстного окна) модель иногда(не всегда), но сбивается на Grep/Glob и пр.
Ну и как бы Rust - это блейзинг(с), тут сказать нечего. Механизм работает, экономит время и токены.
Репозиторий: https://github.com/Regsorm/code-index-mcp
Amareis
Может хоть приведете пример что там в бд то хранится и что агенты запрашивают/получают?..
Regsorm Автор
Данные по БД самого проекта code-index
Из
get_stats, строкаrepo: "code-index"(C:\MCP-Servers\code-index):То есть весь проект — это 108 файлов, разобранных tree-sitter'ом в 992 функции / 152 класса / 413 импортов / 6 372 вызова / 64 переменные. Плюс сырой исходник каждого файла лежит в
file_contents, сжатый zstd.Парсер раскладывает каждый файл на эти таблицы — буквально в функции
write_code_to_db(crates/code-index-core/src/indexer/mod.rs:460): сначалаupsert_file(метаданные: путь, content_hash, ast_hash, mtime, file_size), потомinsert_functions/insert_classes/insert_imports/insert_calls/insert_variables, и в концеupsert_file_content(zstd-исходник). Структура строки в таблицеfunctions(FunctionRecord) совпадает один-в-один с тем, что агент потом получает в ответе.Что агент запрашивает и что получает
Агент даёт два поля — репо и имя символа — и получает точную функцию со строками, а не весь файл (который тут ~2000+ строк). Это и есть экономия токенов из статьи.
Запрос:
Ответ из БД (поля = колонки таблицы
functions, тело сокращено):