Інструментарій для отримання даних RAG: BM25, Dense, HyDE, RRF, MMR та Reranking
Інструментарій для отримання даних 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 разом із моделлю dense чи кількома моделями ембеддингів.
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 — і ніколи не застосовуйте до всього корпусу, адже кожна пара документів потребує окремої обробки. Після виклику LLM це є найбільшим додатковим затримкою у цьому списку, тому він розташований у кінці процесу, а не на початку.
8. Час відповіді: ціна всього цього
Кожна з вищезазначених технік підвищує якість пошуку, проте жодна з них не є безкоштовною. Кожна з них додає затримку, а загальний час від надсилання запиту до отримання остаточної відповіді визначає, чи можна використовувати ретельно налаштований процес у продакшені. Типові витрати за кожен етап:
- Переписування запиту або HyDE, якщо використовується: приблизно від 200 мс до 1 с, додатковий виклик LLM перед початком пошуку.
- Пошук у BM25: близько 5–20 мс, за допомогою дешевого зворотного індексу в пам’яті.
- Пошук щільних векторів (ANN): приблизно 10–50 мс, залежно від розміру та типу індексу — HNSW або плоский.
- Об’єднання за допомогою RRF: 1–5 мс, прості арифметичні операції над списками рангів.
- Переранжування за допомогою MMR: близько 20–100 мс, час зростає з кількістю кандидатів через ітеративний та парний підхід.
- Переранжування за допомогою крос-енкодера, якщо використовується: близько 50–300 мс, часто є найбільшим окремим етапом перед генерацією.
- Генерація за допомогою LLM: від приблизно 500 мс до кількох секунд, зазвичай становить найбільшу частину часу.
Щоб тримати загальний час під контролем:
- Запускайте BM25 та щільне пошукове знаходження паралельно. Вони не залежать один від одного.
- Застосовуйте MMR до невеликої групи — скажімо, до 20–30 найкращих результатів після RRF, оскільки перевірка подвійної схожості має великі витрати на обчислення.
- Зберігайте ембеддинги для частих або повторюваних запитів у кеші.
- Додавайте крос-енкодер лише тоді, коли підвищення точності явно виправдовує затримку.
- Відстежуйте час відповіді як ключовий показник поруч із якістю відповіді, такою як вірність або recall@k. Система, яка на два відсотки точніша, але на три рази повільніша, може не бути готовою до використання.
Як все взаємопов’язано
У продакшн-середовищах гібридні системи пошукового знаходження зазвичай працюють так:
- Переписування запиту або процес HyDE оптимізують нечіткий запит перед пошуком, за бажанням, але це призводить до збільшення затримки.
- BM25 виявляє точні терміни.
- Щільне пошукове знаходження враховує сенс запиту.
- RRF об’єднує обидві списки без необхідності нормалізації оцінок.
- MMR скорочує об’єднаний список до різноманітного набору кандидатів без дублікатів.
- Крос-енкодер проводить фінальну обробку з високою точністю над цим скороченим набором.
- Час відповіді є показником того, чи можна все це використати.
Швидкий посібник з прийняття рішень
- Короткі, нечіткі чи поза словниковим запасом запити? Перепишіть їх або спробуйте HyDE.
- Точна відповідність для кодів, ідентифікаторів та рідкісних термінів? BM25.
- Допускається парафразування? Використовуйте метод щільного пошуку.
- І те, і інше одночасно? Об’єднайте за допомогою RRF.
- У контексті є дублікати чи зайві частини? Використовуйте MMR.
- Найвища релевантність у невеликому фінальному наборі? Переранжування за допомогою крос-енкодера.
- Точний, але повільний результат? Вимірюйте кожен етап перед сліпою оптимізацією.
Жодна окрема техніка не є універсальним рішенням. Найкращі системи обробки даних поєднують їх, налаштовуючи їх на основі реальних журналів запитів, а не інтуїції, причому правильний баланс все ще сильно залежить від корпусу даних та обмежень щодо затримки.