Гибридный поиск и реранкинг
Два приёма, которые заметно поднимают качество retrieval: гибридный поиск и реранкинг.
Гибридный поиск объединяет семантический (векторы) и лексический (ключевые слова, BM25) поиск. Реранкинг переупорядочивает кандидатов более точной моделью.
Зачем гибрид
Векторный поиск ловит смысл, но иногда промахивается на точных терминах: артикулы, коды ошибок, имена («ORA-00942», «pgvector»). Лексический поиск (BM25) наоборот отлично находит точные слова, но не понимает синонимов. Гибрид берёт лучшее от обоих: смысл + точные совпадения.
| Подход | Силён в | Слаб в |
| Векторный | смысл, синонимы, перефраз | точные коды, редкие термины |
| BM25 (слова) | точные термины, артикулы | синонимы, смысл |
| Гибрид | и то, и другое | чуть сложнее настроить |
Смешиваем два скора (запускаемый)
Идея гибрида: нормализуем оба скора в шкалу [0..1] и складываем с весом. Покажем на кандидатах с их векторным скором и числом ключевых попаданий.
docs = [
("Гайд по индексам в PostgreSQL", 0.62, 1), # text, vec, keyword_hits
("PostgreSQL: ускоряем медленные запросы", 0.55, 3),
("Введение в реляционные базы данных", 0.48, 0),
]
def norm(values):
lo, hi = min(values), max(values)
if hi == lo:
return [1.0 for _ in values]
return [(v - lo) / (hi - lo) for v in values]
vec = norm([d[1] for d in docs])
kw = norm([d[2] for d in docs])
alpha = 0.5 # вес векторного скора
fused = []
for (text, _, _), v, k in zip(docs, vec, kw):
score = alpha * v + (1 - alpha) * k
fused.append((round(score, 3), text))
for score, text in sorted(fused, reverse=True):
print(f" {score:.3f} {text}")Вывод:
0.750 PostgreSQL: ускоряем медленные запросы 0.667 Гайд по индексам в PostgreSQL 0.000 Введение в реляционные базы данных
Документ с большим числом ключевых попаданий поднялся наверх, хотя по чистому вектору был вторым. Так гибрид исправляет промахи семантики на точных терминах.
Реранкинг: второй, более точный проход
Векторный поиск быстрый, но грубый: он сравнивает запрос и чанк по отдельности. Реранкер (часто cross-encoder) смотрит на пару «запрос+чанк» вместе и оценивает релевантность точнее. Схема двухступенчатая: дёшево достаём, например, top-50 вектором, затем дорогой реранкер пересортировывает их и оставляет top-5.
candidates = [
("Установка и настройка PostgreSQL", 0.81),
("Резервное копирование базы PostgreSQL", 0.78),
("Копирование таблиц между базами", 0.69),
]
query_words = set("резервное копирование postgresql".split())
def rerank(cands):
out = []
for text, vec in cands:
overlap = len(query_words & set(text.lower().split())) / len(query_words)
final = 0.5 * vec + 0.5 * overlap # имитация более точной оценки
out.append((round(final, 3), vec, text))
out.sort(reverse=True)
return out
print("До реранкинга (по вектору):")
for text, vec in sorted(candidates, key=lambda c: c[1], reverse=True):
print(f" {vec:.2f} {text}")
print("После реранкинга:")
for final, vec, text in rerank(candidates):
print(f" {final:.3f} {text}")Вывод:
До реранкинга (по вектору): 0.81 Установка и настройка PostgreSQL 0.78 Резервное копирование базы PostgreSQL 0.69 Копирование таблиц между базами После реранкинга: 0.890 Резервное копирование базы PostgreSQL 0.572 Установка и настройка PostgreSQL 0.512 Копирование таблиц между базами
Реранкер поднял реально нужный документ с 2-го места на 1-е. Настоящий cross-encoder делает это умнее нашей имитации, но идея та же: дёшево отобрать кандидатов, дорого и точно их переупорядочить.
Итог
- Гибридный поиск = векторы (смысл) + BM25 (точные слова).
- Реранкинг — второй, более точный проход поверх дешёвых кандидатов.
- Оба приёма повышают долю релевантного в верхушке выдачи.