📱 Подписаться
IT и цифровая трансформация

Гибридный поиск по коду в GitLab: как я ускорил поиск по 100+ GitLab-проектам с часов до минут

📰 Habr 👁️ 4 просмотров

HaperStrelkov13 часов назад

Гибридный поиск по коду в GitLab: как я ускорил поиск по 100+ GitLab-проектам с часов до минут

Время на прочтение6 минОхват и читатели5.2KDevOps*Git*Python*КейсИз песочницыКогда проектов в GitLab становится много, довольно быстро появляется одна и та же задача: найти, где используется конкретный API, URL, env-переменная или конфигурационный параметр.

Пока репозиториев мало, всё просто: открыл поиск, ввел строку, получил результат. Но когда проектов уже больше сотни, а нужные вхождения лежат не только в коде, но и в YAML-конфигах, Helm-чартах, .env и JSON-файлах, жизнь становится менее романтичной.

Первый лобовой вариант — просто скачать все проекты локально и искать по ним через grep, ripgrep или IDE. Работает, но тащить 100+ репозиториев на локальную машину ради одной проверки — идея так себе. Ноутбук, скорее всего, энтузиазма не разделит.

Мне хотелось искать прямо поверх GitLab, без локального зеркала всей группы репозиториев. Я начал с просмотра готовых вариантов, а в итоге пришёл к своему гибридному краулеру: код ищется через GitLab API, а конфиги добираются отдельным глубоким обходом файлов. В результате поиск по 100+ проектам сократился с часов до нескольких минут.

Коротко: я не строил “универсальную платформу поиска по коду”. Я решал вполне прикладную инженерную задачу: быстро проверить большую группу проектов и не пропустить то, что лежит в конфигах.Чтобы не перегружать статью полным листингом, я вынес полную версию скрипта в отдельный репозиторий на GitHub. В статье оставил только ключевые фрагменты: логику API Search, Deep Search и параллельную обработку.

Сначала я посмотрел, какие вообще есть варианты

Перед тем как писать своё решение, я прошёлся по вариантам, которые напрашиваются первыми.

Сравнение подходов

ПодходПлюсыМинусыПочему не подошёлGitLab UI SearchБыстро, просто, ничего не нужно настраиватьНеудобно на 100+ проектах, неполно по конфигамДля массовой проверки слишком ручнойЛокальный поиск по клонам (grep, ripgrep)Полный контроль, точный поискНужно клонировать и обновлять все репозиторииСлишком тяжело для разовой или периодической задачиGitLab API SearchБыстро, работает прямо поверх GitLabТе же ограничения индексированного поискаХорошо для кода, но не для всех конфиговSourcegraph и похожие инструментыОчень быстро, удобно, мощноЭто уже отдельный инструмент и отдельное внедрениеСлишком тяжёлое решение под мою задачуСвой гибридный краулерГибко, без отдельной инфраструктуры, можно заточить под задачуНужно написать и поддерживатьОказался лучшим компромиссомЯ не пытался заменить Sourcegraph или построить корпоративный поиск по коду. Задача была заметно проще: быстро и надёжно искать по большой группе GitLab-проектов без лишней инфраструктуры.

Почему я не остановился на одном способе

Очень быстро стало понятно, что разные типы файлов ведут себя по-разному.

Тип файловЧто лучше работаетПочемуКод (.py, .js, .go, .ts)API SearchGitLab нормально индексирует кодДокументация (.md, .txt)API SearchБыстро и обычно этого достаточноКонфиги (.yml, .yaml, .json, .env)Deep SearchЗдесь встроенный поиск чаще промахиваетсяБинарные файлыНе искатьЭто просто лишняя трата времениОтсюда и родилась основная идея: не искать всё одним способом.

Там, где GitLab уже умеет искать быстро, лучше использовать его сильную сторону. Там, где он периодически промахивается, нужно добирать результат вручную.

Идея решения: гибридный поиск

Мой скрипт работает по довольно простой логике:

• Получает список проектов из GitLab-группы.
• Отфильтровывает архивные и неактуальные.
• Для каждого проекта запускает:

• быстрый API Search по коду;
• глубокий обход конфигов.
• Объединяет результаты.
• Формирует отчёты.Получается такая схема:

GitLab Group

├── Получить список проектов

├── Отфильтровать архивные / неактуальные

├── Для каждого проекта:
│ ├── API Search → код, docs, scripts
│ └── Deep Search → yaml, json, env

├── Объединить результаты

└── Сформировать:
├── Детальный отчёт
├── Суммарный отчёт
└── Файл ошибокВ каком-то смысле всё решение строится на одной простой мысли: не делать вид, что один инструмент одинаково хорош во всём.

Как устроен скрипт

1. Сначала получаю список проектов

group = gl.groups.get(GROUP_ID)
all_projects = group.projects.list(include_subgroups=True, all=True)Дальше отбрасываю архивные и неактуальные репозитории, чтобы не тратить на них время.

На практике это важно. Если без фильтрации пройтись по всему подряд, время растёт быстрее пользы.

2. Для кода использую API Search

def api_search(gl, project_id, search_terms):
results = {term: [] for term in search_terms}

for term in search_terms:
blobs = gl.search('blobs', term, project_id=project_id)

for blob in blobs:
file_path = blob.get('path', '')
file_ext = os.path.splitext(file_path)[1].lower()

if file_ext in CODE_EXTENSIONS or file_ext == '':
results[term].append({
'path': file_path,
'url': blob.get('web_url', '#'),
'found_by': 'API'
})

return resultsИменно API Search даёт основную скорость на кодовых файлах и документации.

3. Для конфигов делаю Deep Search

def deep_search_configs(gl, project_id, search_terms):
results = {term: [] for term in search_terms}
project = gl.projects.get(project_id)

files = project.repository_tree(recursive=True, ref='master')

for file in files: if file['type'] != 'blob': continue

ext = os.path.splitext(file['name'])[1].lower()
if ext not in CONFIG_EXTENSIONS:
continue

content = project.files.get(file_path=file['path'], ref='master').decode()
content_lower = content.lower()

for term in search_terms:
if term.lower() in content_lower:
results[term].append({
'path': file['path'],
'found_by': 'Deep'
})

return resultsДа, это медленнее. Но именно здесь добираются YAML, .env и другие конфиги, которые встроенный поиск может пропустить.

Почему поиск только по master

В текущей версии скрипта поиск идёт только по master. Для моей задачи этого было достаточно: внутри компании проверять нужно было именно основную ветку, потому что актуальные конфиги и рабочие настройки лежали там.

Поиск по всем веткам можно добавить позже. Но для первой версии я не хотел усложнять логику и увеличивать время выполнения без необходимости.

Это как раз тот случай, когда сначала лучше закрыть рабочую задачу, а уже потом делать решение более универсальным.

Отдельное ускорение — параллельная обработка

Если обрабатывать 100+ проектов последовательно, скрипт становится заметно медленнее. Поэтому я добавил ThreadPoolExecutor:

with ThreadPoolExecutor(max_workers=3) as executor:
futures = {executor.submit(process_one_project, gl, p): p for p in active_projects}

for future in as_completed(futures):
result = future.result()
results_list.append(result)Это дало хороший прирост по времени без лишнего усложнения.

Без параллельности скрипт тоже работал, но уже без особого энтузиазма. С параллельностью он стал ощущаться как нормальный рабочий инструмент, а не как процесс, который лучше запустить и уйти пить чай.

Как выглядит результат

Скрипт формирует два основных отчёта.

Детальный отчёт

Он показывает:

• проект;
• путь к файлу;
• найденный термин;
• строку;
• способ поиска;
• ссылку на GitLab.Пример:

ПРОЕКТ: service-a Путь: backend/service-a

'SERVICE_EXAMPLE': 2 вхождения
- deploy/prod/values.yml:42 [Deep]
Строка 42: SERVICE_EXAMPLE: "{{ .Values.secrets.apiKey }}"
https://gitlab.example.com/backend/service-a/-/blob/master/deploy/prod/values.yml#L42

Суммарный отчёт

Он показывает:

• сколько проектов затронуто;
• сколько всего вхождений;
• сколько найдено через API;
• сколько найдено через Deep Search.Такой формат оказался удобен не только для самой проверки, но и для дальнейшей работы: результат можно быстро копировать в задачу, тикет или внутреннюю документацию.

Что дало основной выигрыш по времени

Здесь не было одной “волшебной кнопки”. Сработала комбинация из нескольких решений.

Что сделалЧто это далоНе сканировал все файлы подрядУбрал самый тяжёлый сценарийКод оставил на API SearchБыстрый поиск там, где он и так работаетКонфиги вынес в Deep SearchДобрал пропущенные совпаденияДобавил параллельностьСократил общее время обработкиОтсёк архивные проектыНе тратил время впустуюЕсли совсем коротко: я не сделал поиск “умнее”, я сделал его более прагматичным. И этого уже хватило.

Ограничения текущей версии

Чтобы статья не выглядела как “я написал идеальный инструмент”, лучше честно проговорить ограничения.

ОграничениеЧто это значитПоиск только по masterПока не ищу по всем веткамDeep Search ограничен по типам файловЭто сделано ради скоростиНет regexИщу точные вхожденияНет дедупликации API/DeepОдинаковые находки можно улучшить позжеНет отдельного UIСейчас это консольный инструмент

Где такой подход реально полезен

Такой вариант хорошо работает, когда нужно быстро проверить большую группу репозиториев по конкретным терминам.

Например:

• найти использование API-ручки перед изменением контракта;
• проверить, где ещё осталась env-переменная;
• собрать список сервисов, где используется старый URL;
• понять, в каких конфигах ещё не обновлён параметр;
• быстро оценить влияние изменения какого-то внешнего сервиса.Особенно хорошо такой подход заходит в сценариях “нужно проверить сейчас”, а не “давайте строить отдельную поисковую платформу на квартал”.

Что можно улучшить дальше

Следующая версия напрашивается сама собой:

• определять default branch автоматически;
• добавить поиск по нескольким веткам;
• поддержать regex;
• дедуплицировать результаты;
• добавить CSV / JSON-экспорт;
• аккуратнее обработать rate limits и retries;
• сделать небольшой CLI или web-интерфейс.То есть потенциал роста есть. Но даже текущая версия уже решает ту задачу, ради которой всё затевалось.

Итог

Встроенный поиск GitLab оказался хорош не везде одинаково: по коду — нормально, по конфигам — уже не так надёжно. Поэтому я не стал пытаться искать всё одним способом и сделал гибридный подход:

• код — через API Search;
• конфиги — через прямой обход файлов;
• результаты — в один отчёт;
• обработку — в несколько потоков.В итоге поиск по 100+ GitLab-проектам перестал быть многочасовой рутиной и стал обычной технической операцией на несколько минут.

Главный вывод здесь довольно простой: не всегда нужно строить большой универсальный инструмент. Иногда достаточно честно разложить задачу на части и для каждой выбрать самый практичный способ.

Если у вас были похожие сценарии с GitLab-группами, конфигами или поиском по большой кодовой базе, интересно было бы сравнить подходы. В таких задачах чужие инженерные костыли обычно не менее полезны, чем свои.Теги:• краулер
• поиск
• проект
• гитлабХабы:• DevOps
• Git
• Python

Получайте больше инсайтов о систематизации бизнеса

Подписывайтесь на Telegram-канал Business Operations — ежедневные материалы о бизнес-процессах, операционном управлении и повышении эффективности

💬 Подписаться на канал