Агент-инженер по репозиторию · Модуль 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 этого проекта.

retrieval.go: интерфейс Retriever и слияние RRF (новый файл, фрагмент)
+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, а не складывают их сырые оценки?

  • A RRF быстрее вычисляется
  • B У BM25 и косинуса разные шкалы оценок; RRF работает с рангами и устойчив без ручной калибровки весов
  • C Сырые оценки складывать нельзя в Go
  • D RRF не требует индекса

Запрос «где валидируется путь?» должен найти функцию safeJoin, в которой нет слова «валидируется». Какая ветвь гибрида это обеспечивает?

  • A Лексическая (BM25) — по точному совпадению слов
  • B Семантическая (эмбеддинги) — по смысловой близости запроса и кода, даже без общих слов
  • C Никакая, это невозможно
  • D grep из версии v1