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







