Инструментарий для поиска информации в RAG: BM25, Dense, HyDE, RRF, MMR и повторная ранжирование
Инструментарий для поиска информации в RAG: BM25, плотные векторы, HyDE, RRF, MMR и повторная классификация, когда использовать каждый из них, приблизительное время обработки на каждом этапе и как гибридная схема объединяет их.
Источники: The Retrieval Toolkit That’s Quietly Deciding Whether Your RAG Pipeline Works.
После использования нескольких пайплайнов типа RAG вы, как правило, сталкиваетесь с одними и теми же проблемами. Эмбеддинги хороши, промпты чистые, но ответы всё равно бывают некорректными. Часто вина не в самой модели, а в том, что ей предоставляется. Ключевой проблемой является процесс поиска информации, и большинство сбоев связано с ограниченным набором методов, которые команды игнорируют или неправильно применяют.
Вот этот набор инструментов: как работает каждый элемент, зачем он нужен, когда его использовать, как они сочетаются в производственном пайплайне, а также сколько времени требуется на получение ответа.
1. BM25: функция оценки ключевых слов, которая никогда не устареет
BM25 (Best Matching 25) — это разреженная функция лексического ранжирования, улучшенная версия алгоритма TF-IDF. Она ранжирует документы в зависимости от количества совпадающих с запросом терминов с учётом частоты терминов и длины документа.
За каждый поисковый термин документ получает баллы, если этот термин встречается часто, но эффективность увеличения баллов снижается: двадцать вхождений совсем не делают документ в двадцать раз более релевантным, чем одно вхождение. Длинные документы штрафуются, чтобы они не могли накапливать совпадения просто из-за большого объема текста.
from rank_bm25 import BM25Okapi
corpus = [doc.split() for doc in documents] # tokenized docs
bm25 = BM25Okapi(corpus)
query = "vector database indexing".split()
scores = bm25.get_scores(query)
top_k = sorted(range(len(scores)), key=lambda i: -scores[i])[:5]
BM25 учитывает сами токены, а не их значение, что делает его эффективным для:
- Кодов продуктов, SKU, кодов ошибок и идентификаторов
- Именованных сущностей, таких как люди, компании и названия лекарств
- Редких или технических терминов, которые при использовании эмбеддингов склонны сливаться
Используйте его там, где важна точность определенных терминов больше, чем возможность парафразирования: в юридических, медицинских, кодовых и лог-поисках. Он дешев и быстр, не требует обучения и GPU. Его слабость — отсутствие семантического понимания, поэтому «автомобиль» и «машина» кажутся ему не связанными терминами.
2. Поиск с плотными векторами: сопоставление значений
Метод плотного поиска преобразует запросы и документы в векторы эмбеддингов с сотнями или тысячами измерений без нулей; эти векторы генерируются нейронным кодером. Сходство определяется с помощью косинусного или скалярного произведения, а приблизительный индекс ближайших соседей, такой как HNSW или IVF, находит наиболее близкие кандидаты.
from sentence_transformers import SentenceTransformer
import numpy as np
model = SentenceTransformer("BAAI/bge-small-en-v1.5")
doc_embeddings = model.encode(documents, normalize_embeddings=True)
query_embedding = model.encode([query], normalize_embeddings=True)
scores = np.dot(doc_embeddings, query_embedding.T).flatten()
top_k = np.argsort(-scores)[:5]
Он позволяет определять семантическое сходство: запрос «отставка руководителя» находит результат «генеральный директор ушел в отставку», несмотря на отсутствие общих слов. Люди часто называют это семантическим поиском, хотя строго говоря механизмом является плотный поиск, а семантическое сопоставление — его результатом.
Его можно использовать при обращении с разговорными запросами, перефразированными вопросами, межъязычным поиском или в тех случаях, когда пользователи не будут использовать точный словарный состав ваших документов. Он может давать неточные результаты для редких терминов, точных кодов и чисел, поскольку пространство вкладок сглаживает детали, которые сохраняет алгоритм BM25.
3. Переписывание запросов и HyDE: первичная коррекция запроса
Иногда проблема не в самом механизме поиска, а в формулировке запроса. Пользователи вводят краткие, нечеткие или странно сформулированные вопросы, тогда как как BM25, так и плотный поиск работают наилучшим образом с хорошо структурированным, информативным текстом.
Переписывание запроса предполагает, что ЯЗЫК расширяет или уточняет вопрос перед поиском, превращая фразу «насколько быстро» в точный вопрос о том, сколько документов в секунду может обрабатывать индексационный процесс X в момент пиковой нагрузки.
HyDE (Hypothetical Document Embeddings) идет еще дальше. Вместо того чтобы встраивать запрос в пространство векторов, от ЯЗЫКА требуется придумать правдоподобный ответ, после чего этот вымышленный ответ встраивается в пространство векторов и используется для поиска реальных документов. Идея заключается в том, что выдуманный ответ часто находится в пространстве векторов ближе к настоящим ответам, чем сам вопрос.
hypothetical_doc = llm.generate(f"Write a short passage answering: {query}")
hyde_embedding = model.encode([hypothetical_doc], normalize_embeddings=True)
scores = np.dot(doc_embeddings, hyde_embedding.T).flatten()
Короткие или неоднозначные запросы являются одной из наиболее распространенных причин плохой точности поиска, поскольку такие запросы просто не несут достаточного количества информации. Это особенно актуально для интерфейсов чат-типа и в тех областях, где словарный запас пользователей отличается от словаря корпуса. Недостатком является необходимость дополнительного вызова нейросети перед началом поиска, что увеличивает задержку и затраты, а качественно плохой гипотетический ответ может направить поиск в неверном направлении.
4. Гибридный поиск: использование обоих методов
Метод BM25 и метод плотного поиска терпят неудачу в противоположных ситуациях. Гибридный поиск выполняет оба метода параллельно и объединяет списки результатов, позволяя находить точные термины и парафразы за один проход. Однако оценки BM25 и косинусные сходства существуют на разных шкалах, поэтому их среднее значение неэффективно. Именно для этого используется метод слияния рангов по принципу взаимности.
5. Слияние рангов по принципу взаимности: объединение рангов, а не оценок
RRF полностью избегает нормализации баллов, объединяя значения в зависимости от рангового положения и игнорируя исходные баллы.
RRF_score(doc) = Σ 1 / (k + rank_r(doc)) for each ranker r
Константа k, обычно равная 60, снижает интенсивность влияния, чтобы один рейтинг первого места не мог доминировать.
def reciprocal_rank_fusion(ranked_lists, k=60):
scores = {}
for ranked_list in ranked_lists:
for rank, doc_id in enumerate(ranked_list):
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
return sorted(scores.items(), key=lambda x: -x[1])
bm25_ranked = ["doc3", "doc1", "doc5"]
dense_ranked = ["doc1", "doc4", "doc3"]fused = reciprocal_rank_fusion([bm25_ranked, dense_ranked])
Документы, которые получают высокие оценки от нескольких поисковых систем, естественным образом попадают в верхнюю часть списка без необходимости калибровки шкал оценок. Это стандартный способ объединения гибридных результатов и подходит для любых случаев, когда объединяются два или более ранжированных списка от разных поисковых систем, например BM25 в сочетании с другими методами или несколько моделей эмбеддингов.
6. Максимальная маржинальная релевантность: устранение избыточности
Даже идеальное ранжирование может оказаться бесполезным, если пять лучших результатов говорят об одном и том же. Метод MMR переранжирует элементы с учётом как релевантности, так и разнообразия информации.
MMR = λ · Sim(doc, query) − (1 − λ) · max(Sim(doc, selected))
Параметр λ определяет баланс. При λ = 1 используется обычный метод top-k, а при λ = 0 максимизируется разнообразие независимо от релевантности.
import numpy as np
def mmr(query_vec, doc_vecs, doc_ids, lambda_param=0.5, top_k=5):
selected, remaining = [], list(range(len(doc_ids)))
sim_to_query = doc_vecs @ query_vec
while len(selected) < top_k and remaining:
if not selected:
idx = remaining[np.argmax(sim_to_query[remaining])]
else:
mmr_scores = []
for i in remaining:
sim_to_selected = max(doc_vecs[i] @ doc_vecs[j] for j in selected)
mmr_scores.append(
lambda_param * sim_to_query[i] - (1 - lambda_param) * sim_to_selected
)
idx = remaining[np.argmax(mmr_scores)]
selected.append(idx)
remaining.remove(idx)
return [doc_ids[i] for i in selected]
Без технологии MMR можно потратить весь объем окна контекста на пять почти идентичных фрагментов вместо пяти фрагментов, охватывающих разные аспекты ответа. Это эффективно для корпусов с перекрывающимся или повторяющимся контентом, таких как страницы FAQ, документация к продуктам с шаблонными текстами или любые наборы почти идентичных отрывков.
7. Переранжирование с использованием кросс-энкодера: ключ к высокой точности
Всё, что было рассказано до сих пор, относится к подходу с двойным энкодером, при котором запросы и документы эмбеддингуются отдельно, а затем сравниваются. Кросс-энкодер передает запрос и кандидатский фрагмент в один трансформер, что позволяет модели учитывать оба текста. Это обеспечивает гораздо более точные оценки релевантности по сравнению с сравнением заранее вычисленных векторов.
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
pairs = [(query, doc) for doc in candidate_docs]
scores = reranker.predict(pairs)
reranked = [doc for _, doc in sorted(zip(scores, candidate_docs), reverse=True)]
Кросс-энкодеры постоянно превосходят би-энкодеры по показателю схожести при оценке релевантности, поскольку они одновременно анализируют запрос и документ. Используйте их для окончательной обработки небольшого набора кандидатов — обычно топ-20–50 результатов после фьюжн-обработки или метода MMR — но никогда не применяйте их ко всему корпусу документов, так как каждая пара документов требует отдельной обработки. Вызов большой языковой модели сам по себе является основным фактором увеличения задержки, поэтому он находится в конце процесса, а не в его начале.
8. Время ответа: цена всего этого
Каждая из вышеупомянутых техник повышает качество поиска, но ни одна из них не бесплатна. Каждая из них увеличивает задержку, и общее время от отправки запроса до получения окончательного ответа определяет, можно ли использовать тщательно настроенную систему в производственных условиях. Типичные затраты на каждый этап:
- Переписывание запроса или использование HyDE: примерно от 200 мс до 1 секунды, дополнительный вызов большой языковой модели перед началом поиска.
- Поиск с использованием BM25: примерно от 5 до 20 мс, с использованием недорогого обратного индекса в памяти.
- Поиск плотных векторов (ANN): примерно от 10 до 50 мс, в зависимости от размера и типа индекса — HNSW или плоский.
- Слияние с помощью RRF: от 1 до 5 мс, простые арифметические операции над списками рангов.
- Переранжирование с использованием MMR: примерно от 20 до 100 мс, время растет с увеличением количества кандидатов из-за итеративного и парного характера процесса.
- Переранжирование с использованием кросс-энкодера, если применяется: примерно от 50 до 300 мс, часто является самой значительной статьей затрат после генерации.
- Генерация с помощью большой языковой модели: от примерно 500 мс до нескольких секунд, обычно составляет наибольшую долю времени.
Чтобы контролировать общее время:
- Запускайте BM25 и метод плотного поиска параллельно. Они не зависят друг от друга.
- Применяйте метод MMR к небольшой группе результатов, например, к 20–30 лучшим после RRF, поскольку проверки взаимной похожести имеют высокую сложность.
- Храните эмбеддинги для часто задаваемых или повторяющихся запросов в кэше.
- Добавляйте кросс-кодер только тогда, когда улучшение точности явно оправдывает возникающую задержку.
- Отслеживайте время ответа как важный показатель наряду с качеством ответа, таким как точность или recall@k. Система, на два процента более точная, но в три раза медленнее, может не стоить выпуска.
Как все соединяется
В производственных гибридных системах поиска обычно работает следующий алгоритм:
- При необходимости перед поиском происходит переписывание запроса или его оптимизация с помощью HyDE для уточнения нечеткого запроса, что может увеличить задержку.
- BM25 находит точные соответствия терминам.
- Метод плотного поиска находит соответствия на основе смысла.
- RRF объединяет оба списка без необходимости нормализации оценок.
- MMR сокращает объединенный список до набора кандидатов, состоящего из разнообразных элементов без дубликатов.
- Кросс-кодер проводит окончательную обработку с высокой точностью над этим уменьшенным набором.
- Время ответа служит показателем того, можно ли использовать все полученные результаты.
Краткое руководство по принятию решений
- Короткие, нечеткие или не соответствующие словарю запросы? Перепишите их или попробуйте HyDE.
- Требуется точное совпадение для кодов, идентификаторов и редких терминов? Используйте BM25.
- Допускается парафразирование? Применяйте метод плотного поиска.
- Нужно сделать и то, и другое одновременно? Используйте метод RRF для объединения.
- В контексте присутствуют дублирующиеся фрагменты? Применяйте метод MMR.
- Необходима максимальная релевантность среди небольшого финального набора? Используйте кросс-кодер для переранжирования.
- Результаты точные, но медленные? Измерьте производительность каждого этапа перед тем, как начинать слепую оптимизацию.
Нет единственного универсального решения. Лучшие системы обработки данных сочетают в себе различные методы, настроенные на основе реальных журналов запросов, а не интуиции; при этом оптимальное сочетание во многом зависит от объема данных и лимита задержек.