Агент-инженер по репозиторию · Модуль 3 · Урок 3.3
Гибридный поиск: BM25 + эмбеддинги (старт от BM25-lite проекта)
Зачем (какую проблему чиним)
grep находит точные совпадения, но мимо синонимов и перефразировок («где мы валидируем путь?» не найдёт safeJoin, если в нём нет слова «валидируем»). Чисто векторный поиск, наоборот, ловит смысл, но мажет на точных идентификаторах и редких токенах. Нужен гибрид.
Решение и альтернативы
Решение: объединяем лексический поиск (BM25) и семантический (эмбеддинги) через слияние рангов (reciprocal rank fusion, RRF). Лексическую часть переиспользуем как старт из проекта: в assistant/search.go уже есть рабочий BM25-lite (BuildIndex, Search, токенизация, IDF, параметры k1/b) — берём его как основу для индекса по чанкам кода. Семантическую часть добавляем как второй ретривер за общим интерфейсом Retriever.
Альтернативы: только BM25 (как grep++) — дёшево, но без семантики; только вектор — мажет на идентификаторах; сразу re-ranking cross-encoder'ом — точнее, но дороже и сложнее, оставляем как улучшение. Гибрид BM25+вектор с RRF — лучший баланс на старте.
DIFF
Вводим интерфейс Retriever, лексический ретривер на базе BM25-lite из проекта и слияние RRF; оформляем поиск как инструмент search_code.
⚠ Безопасность
Индексируем только дерево репозитория (через safeJoin и игнор-лист из версии v1): .git, .env, ключи в индекс не попадают. Эмбеддинг-запросы уходят к провайдеру — помним, что содержимое кода покидает периметр; для приватных репозиториев это осознанный выбор (или локальная модель эмбеддингов).
Проверка
«Где валидируется путь файла?» → search_code возвращает чанк safeJoin в топе, хотя слова «валидация» в нём нет, — заслуга семантической ветви. Точечный запрос по имени runGit находит лексическая ветвь.
Глубже
Гибридный поиск, RRF, re-ranking, переписывание запросов, агентный RAG — курс «Продакшн-разработка», Модуль 2 (гл. 3). Реализация BM25-lite, на которой стартуем, — assistant/search.go этого проекта.
+package main
+
+// Retriever — общий контракт ретривера: вернуть ранжированные чанки.
+type Retriever interface {
+ Search(query string, k int) []Chunk
+}
+
+// lexical — BM25-ретривер. За основу взят BM25-lite из assistant/search.go
+// проекта (tokenize, df/idf, параметры k1=1.5, b=0.75), адаптированный под
+// чанки кода (Chunk вместо учебных секций).
+type lexical struct{ idx *bm25Index }
+
+// semantic — векторный ретривер (эмбеддинги чанков и запроса; косинус).
+type semantic struct{ store *vectorStore }
+
+// hybrid сливает ранги двух ретриверов через RRF: устойчиво к разным шкалам
+// оценок BM25 и косинуса, не требует калибровки весов.
+func hybrid(query string, k int, rs ...Retriever) []Chunk {
+ const rrfK = 60.0
+ score := map[string]float64{}
+ pick := map[string]Chunk{}
+ for _, r := range rs {
+ for rank, c := range r.Search(query, k) {
+ id := c.Path + "#" + c.Symbol
+ score[id] += 1.0 / (rrfK + float64(rank+1))
+ pick[id] = c
+ }
+ }
+ return topByScore(pick, score, k)
+}Anti-patterns
| Грабля | Почему плохо | Как правильно |
|---|---|---|
| Складывать сырые оценки BM25 и косинуса напрямую | Разные шкалы; одна ветвь доминирует, веса приходится подбирать вручную | Слияние по рангам (RRF): устойчиво к шкалам, без калибровки весов |
| Только вектор для поиска по коду | Мажет на точных идентификаторах, флагах, редких токенах | Гибрид: лексическая ветвь ловит точные имена, семантическая — смысл |
| Индексировать .git/.env/ключи | Секреты попадают в индекс, контекст и эмбеддинг-запросы | Игнор-лист + safeJoin; для приватного кода — осознанный выбор провайдера эмбеддингов |
Практическое задание (RA-v2)
- Адаптировать BM25-lite из
assistant/search.goпод индекс поChunk; обернуть вRetriever. - Добавить векторный ретривер (эмбеддинги) и слияние через RRF; оформить как инструмент
search_code. - Закоммитить:
git commit -m "v2: hybrid BM25+embeddings retrieval".
Проверка знаний
Почему ранги BM25 и векторного поиска сливают через RRF, а не складывают их сырые оценки?
Верный ответ: B
B. Оценки BM25 и косинусной близости несопоставимы по шкале — прямое сложение даёт перекос в пользу одной ветви и требует подбора весов. RRF оперирует позициями (рангами) и устойчиво объединяет разнородные источники.
Запрос «где валидируется путь?» должен найти функцию safeJoin, в которой нет слова «валидируется». Какая ветвь гибрида это обеспечивает?
Верный ответ: B
B. Семантическая ветвь сопоставляет смысл запроса и содержимого, поэтому находит safeJoin по близости понятий, даже когда нет общих слов. Лексическая ветвь (A) и grep (D) тут промахнулись бы.