Индексация кодовой базы: почему поиск по коду не работает
Наивный поиск по коду опирается на разбиение файлов на части фиксированного размера — по числу строк или символов, как это делают универсальные системы семантического поиска. В монорепозитории такая схема ломается сразу: функция на сорок строк распадается на три куска, контекст теряется, а в выдачу попадает середина класса без сигнатуры и без места вызова.
Релевантность выдачи падает и на уровне векторных представлений. Вектор фрагмента не знает, что перед ним метод сервиса, а не конфиг или тест, что имя переменной здесь — тип домена, а не строка. Поиск уверенно возвращает похожие по набору слов куски, но не те, что связаны со смыслом задачи. Разработчик получает десяток файлов и вручную выясняет, какой вообще относится к делу.
Дальше подключается языковая модель и добивает картину: на неполном контексте она уверенно достраивает несуществующие методы, придумывает параметры и ссылается на классы, которых в проекте нет. Проверка такой подсказки занимает больше времени, чем написать код самому, — инструмент превращается в источник ложных сигналов, а доверие к нему падает до нуля.
Корень проблемы не в модели и не в размере индекса, а в том, что код — структурированный артефакт, а его индексируют как обычный текст. Разбор синтаксического дерева даёт границы, совпадающие с реальными сущностями: функция, класс, модуль, область видимости. Ниже — как это меняет качество поиска.
Что даёт разбор синтаксиса: измеримый эффект
При индексации без разбора синтаксиса файл режется на окна фиксированного размера, и в выдачу попадают обрывки: половина функции, чужая сигнатура, кусок условия. Модель по такому фрагменту отвечает неточно или достраивает недостающее — отсюда ложные совпадения и ссылки на код, которого в проекте нет. Tree-sitter строит дерево разбора, и единицей индекса становится целая функция, класс или метод.
Точность выдачи растёт потому, что совпадение привязано к осмысленному узлу, а не к строке. Поиск по имени метода в разных компонентах даёт разные узлы с разным окружением, и контекст вызова отсекает ложные совпадения до того, как они дойдут до модели. В верхней части выдачи почти не остаётся мусора из разорванных блоков.
Скорость поиска нужного фрагмента — это навигация по дереву: сразу переходим к определению и его вызовам вместо ручного листания файлов. Число обращений к первоисточнику падает, потому что инженеру реже нужно открывать репозиторий и проверять, что имела в виду модель.
Отдельный класс ошибок — «модель сослалась на несуществующий код» — закрывается тем, что каждый узел хранит координаты в файле. Ссылку можно проверить, а фрагмент, которого нет, в выдачу не попадает. Разбор синтаксиса переводит поиск по коду из «похоже на то» в «этот метод в этом файле».
Способы разбора кода: сравнение подходов
Выбор способа разбора определяет, какие вопросы вообще сможет закрыть поиск по репозиторию. Индексация кодовой базы — это всегда компромисс между точностью структуры и устойчивостью к разнородному коду: строгий разбор даёт связи между сущностями, но ломается на диалектах и сгенерированных файлах, а размытый работает везде, но возвращает шум.
Отдельно решается гранулярность разбиения: часть файла, функция, класс или модуль целиком. Чем крупнее единица, тем больше контекста попадает в ответ и тем выше шанс, что нужная деталь в нём утонет.
| Вариант | Когда подходит | Ограничения |
|---|---|---|
| Плоский поиск по тексту файлов | Точные имена, пути, редкие строки конфигураций | Не различает смысл, ловит совпадения в комментариях, синонимы не находит |
| Векторный поиск по частям файла | Смысловые вопросы, разнородный код, много языков программирования | Гранулярность разбиения задана формально: функции рвутся, в выдаче много близких по теме, но нерелевантных фрагментов |
| Разбор в синтаксическое дерево | Однородная кодовая база, важны связи «определение — вызов — зависимость» | Нужна поддержка разбора для каждого языка; на диалектах и шаблонном коде разбор частично теряется |
| Гибридная схема | Большие репозитории, вопросы «где это используется и что сломается» | Сложнее во внедрении, требует пересчёта индекса при изменениях |
Гибридная схема обычно и даёт рабочий результат: кандидаты набираются лексическим и векторным поиском, а границы блоков уточняются по синтаксическому дереву. Качество проверяется долей ответов с корректной ссылкой на файл и строку — это и есть критерий, по которому подходы стоит сравнивать на вашем коде.
Этапы внедрения: от аудита репозиториев до передачи
Индексация кодовой базы — не разовый скрипт, а регламент, который живёт вместе с репозиториями. Качество ответов ассистента определяется тем, как код нарезан на фрагменты и насколько свежа выдача, поэтому работу ведём этапами и на каждом фиксируем проверяемый результат.
Последовательность работ
- Аудит репозиториев. Считаем число репозиториев, языков и объём кода, отделяем генерируемые и вендорные каталоги, помечаем заброшенные ветки: мусор в индексе даёт ложные срабатывания.
- Проектирование схемы разбора. Определяем единицу смысла — функция, класс, модуль. Нарезка идёт по синтаксическому дереву с сохранением связей между вызовами, а не по фиксированному числу строк.
- Сборка индекса. Первичная сборка по всей кодовой базе с привязкой к коммиту: любую выдачу можно сопоставить с конкретной версией кода.
- Настройка обновления. Индекс перестраивается по событию в системе контроля версий — точечно, по изменённым файлам, а не целиком.
- Замеры качества. Готовим эталонный набор вопросов к кодовой базе, считаем долю попаданий в нужный модуль и полноту контекста в ответе.
- Разграничение доступа. Права наследуются от репозиториев и веток: поиск не отдаёт код, который сотруднику не положено видеть.
- Передача документации. Отдаём регламенты обновления, схему разбора и метрики, по которым команда сама контролирует качество индекса.
После передачи индекс остаётся управляемым: понятно, что чинить при падении качества и как подключать новые репозитории без пересборки с нуля.
Артефакты внедрения: схемы, правила доступа, регламенты
Внедрение заканчивается не демонстрацией поиска на подготовленных вопросах, а моментом, когда команда заказчика сама подключает новый репозиторий, меняет правила доступа и обновляет индекс без нас. Поэтому мы передаём не «работающий сервис», а комплект артефактов, где каждый закрывает свой участок эксплуатации.
- Схема индекса. Какие сущности вычленяются из кода (модуль, класс, функция), как связываются по вызовам и наследованию, где лежат метаданные и векторные представления фрагментов. По ней видно, почему поиск находит определение функции по косвенному описанию.
- Карта разбора. Перечень языков и грамматик, которые разбираются в синтаксическое дерево, и что попадает в индекс как обычный текст. Отдельно отмечены участки неполного разбора — чтобы от поиска не ждали невозможного.
- Правила доступа. Какие репозитории, ветки и каталоги видны каждой команде, на каком слое проверяется право и как ограничение отражается в выдаче. Права наследуются из существующих групп, а не заводятся вторым списком.
- Регламент обновления. Что запускает переиндексацию (слияние в основную ветку, расписание, ручной запуск), сколько она длится, что происходит с индексом при удалении кода и переименовании файлов. Плюс порядок полной пересборки, если схема разбора изменилась.
- Журнал замеров качества. Эталонный набор вопросов к кодовой базе с ожидаемыми ответами и отчёт по доле попаданий до и после правок. Изменения весов и разбора проверяются на нём, а не на ощущениях.
- Инструкции для разработчиков. Как формулировать запрос, чтобы поиск вернул нужный фрагмент, как добавить новый репозиторий, куда смотреть в логах при неверном результате.
- Эксплуатационные материалы. Конфигурации, схема развёртывания, точки мониторинга и порядок отката на предыдущую версию индекса — этого хватает, чтобы дежурная смена подняла систему после сбоя без нас.
Кейс: поиск по монорепозиторию до и после
Монорепозиторий на несколько тысяч пакетов и десятки миллионов строк: ответ на вопрос «где это определено и кто это вызывает» занимал у инженера до семи минут — с переключением между каталогами, чтением похожих по имени сущностей и ручной проверкой. Полнотекстовый поиск по строкам цеплял совпадения в комментариях, фикстурах и сгенерированном коде, поэтому в топ-5 релевантных попаданий оказывалось меньше трети.
Что изменили
Добавили разбор структуры через Tree-sitter: обход файлов, построение дерева разбора и индекса сущностей — модулей, классов, методов, вызовов и импортов с привязкой к диапазонам строк. Запрос идёт не по тексту, а по графу связей: «кто вызывает этот метод», «где реализован интерфейс», «что затронет смена сигнатуры». Переиндексация инкрементальная: после коммита обновляется только затронутая ветвь дерева, полный проход не требуется.
Метрики до и после
| Метрика | До | После |
|---|---|---|
| Время найти нужный участок | около 7 минут | около 40 секунд |
| Релевантных совпадений в топ-5 | 31% | 82% |
| Ошибок «ссылка на несуществующий метод» | около 35 в неделю | единичные |
Разграничение доступа наследуется из системы контроля версий: индекс хранит те же права на файл, что и сам репозиторий, поэтому закрытые модули не всплывают в выдаче у тех, кому доступ к ним не выдан. Роли и группы не дублируются в отдельном списке — при переносе сотрудника между командами права на поиск меняются вместе с правами на код.
Чем инкрементальная индексация отличается от полной пересборки?
Полная пересборка обходит всю кодовую базу целиком: каждый файл заново разбирается, режется на фрагменты и векторизуется. Время такой операции зависит от размера репозитория, а не от объёма правки — в монорепозитории это десятки минут, и всё это время поиск отдаёт вчерашний срез кода.
Инкрементальное обновление стартует от изменений в коммите или слиянии ветки. Разбор через Tree-sitter даёт дерево разбора, и по нему видно, какие функции, классы и модули действительно затронуты: пересчитываются только изменённые узлы, а остальные фрагменты и их векторы переиспользуются как есть.
У инкремента есть границы применимости, и они важнее самой оптимизации. Обновлять по изменениям нельзя при смене модели векторизации, схемы нарезки на фрагменты или добавлении нового языка: старые и новые векторы окажутся в несогласованных пространствах, и близость перестанет что-либо значить. Удалённые файлы отслеживаются записями-надгробиями — иначе выдача возвращает код, которого в репозитории уже нет.
По нагрузке разница тоже ощутима: инкремент дописывает новые сегменты вместо перестройки хранилища целиком, поэтому пиковая запись идёт фоном и не конкурирует с поиском. Периодическое сжатие по расписанию убирает мусор от удалённых и переписанных файлов.
На практике мы держим гибрид: инкремент после каждого слияния в основную ветку, полная пересборка — по расписанию и обязательно при смене модели или схемы разбора. Свежесть выдачи тогда измеряется минутами, а не часами.
Технические сомнения перед стартом проекта
Перед стартом сомнения обычно одни и те же: не превратится ли общий индекс в дыру в правах, что будет после переписывания истории, потянем ли объём и языки, придётся ли ломать уже работающее. Ниже — как эти вопросы закрываются на практике.
Индекс общий — не получится ли, что код утечёт между командами?
Индекс собирается отдельным слоем, а права берутся из системы контроля версий и наследуются по путям. Фильтрация по доступу выполняется до выдачи фрагментов, а не после: в ответ модели не попадают куски из репозиториев, закрытых для пользователя. Состав групп пересчитывается при каждом изменении, поэтому выдача всегда соответствует текущим правам.
Что происходит при переписывании истории и принудительной перезаписи ветки?
Индексирование привязано к коммитам, а не к «текущему состоянию файлов». При переписывании истории пересобираются только затронутые пути, остальное не трогаем. Устаревшие версии фрагментов помечаются неактуальными и удаляются по правилу хранения — поиск не выдаёт код, которого в основной ветке уже нет.
У нас несколько языков и большой монорепозиторий — потянет ли?
Разбор идёт по синтаксическому дереву для каждого языка отдельно, поэтому связи между функциями и классами не размываются на смешанном коде. Файлы без разборщика обрабатываются резервным способом — по разделам и отступам. Монорепозиторий индексируется частями: приоритет у активных веток, остальное догоняет в очереди.
У нас уже развёрнут свой поиск по коду — всё придётся переделывать?
Нет. Мы сверяем покрытие существующего решения, подключаем его как источник и дополняем там, где не хватает структурного разбора. Полная замена нужна только если текущий поиск не отдаёт фрагменты с привязкой к файлу и строке.
Обсудим задачу: вернёмся с оценкой и планом
Постановка задачи у нас начинается с вводных, а не с коммерческого предложения: чем точнее исходные данные, тем меньше допущений в оценке объёма работ и тем реалистичнее порядок этапов. Для первичного разбора достаточно такого набора:
- доступ к репозиторию или выгрузку и список языков, которые нужно покрыть индексацией кодовой базы;
- структуру каталогов и правила сборки — по ним видно, как код делится на модули и сервисы;
- примеры вопросов, на которые решение должно отвечать: поиск по функции, трассировка вызовов между модулями, объяснение незнакомого участка;
- ограничения по доступам: какие каталоги, ветки и файлы не должны попадать в индекс;
- чем не устраивает текущий поиск по коду — скорость, полнота выдачи, отсутствие контекста;
- желаемые точки, где нужно ваше согласование этапов.
На созвоне разберём границы индексации, частоту переиндексации при изменениях в ветках, требования к качеству выдачи и способ подключения к рабочим инструментам команды.
По итогам заказчик получает схему индексации с разбором кода на синтаксическом дереве, порядок работ по этапам и план внедрения — с точками согласования и измеримым результатом каждого этапа.
Опишите задачу в двух-трёх абзацах — вернёмся с оценкой объёма работ и планом внедрения.







