Narzędzia do wyszukiwania RAG: BM25, Dense, HyDE, RRF, MMR i ponowna sortowanie
Narzędzia do wyszukiwania w RAG: BM25, wektory gęste, HyDE, RRF, MMR oraz ponowna sortowanie, kiedy stosować każde z nich, przybliżony czas reakcji na każdym etapie oraz to, jak hybrydowy pipeline je łączy.
Źródła: The Retrieval Toolkit That’s Quietly Deciding Whether Your RAG Pipeline Works.
Po zastosowaniu kilku pipeline’ów typu RAG szybko napotykamy ten sam problem. Embeddingi są dobre, a prompt jasny, mimo to odpowiedzi wciąż są niewystarczające. Często winą nie jest model, lecz to, co mu dostarczono. Kluczowym problemem jest proces wydobywania informacji, a większość niepowodzeń wynika z zaniedbania lub niewłaściwego stosowania ograniczonej liczby technik.
Oto ten zestaw narzędzi: jak każdy element funkcjonuje, dlaczego istnieje, kiedy go stosować oraz jak łączą się one w pipeline produkcyjny, a także ile czasu zajmuje im udzielenie odpowiedzi.
1. BM25: metoda oceny słów kluczowych, która nie umiera
BM25 (Best Matching 25) to funkcja rankingu leksykalnego o niskiej gęstości informacji, stanowiąca ulepszenie metody TF-IDF. Ranking dokumentów opiera się na liczbie wspólnych terminów z zapytaniem, z korektą uwzględniającą częstotliwość występowania terminów i długość dokumentu.
Dla każdego terminu z zapytania dokument otrzymuje punkty, gdy termin ten występuje często, ale z malejącą efektywnością: dwadzieścia wystąpień wcale nie są dwadzieścia razy bardziej istotne niż jedno. Długie dokumenty są karane, aby nie mogły uzyskać wielu dopasowań tylko dlatego, że są rozwlekłe.
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 skupia się na dosłownym tokenie, a nie na znaczeniu, co czyni go skutecznym w przypadku:
- kodów produktów, SKU, kodów błędów i identyfikatorów
- entytetów nazwanych, takich jak osoby, firmy i nazwy leków
- rzadkich lub technicznych terminów, które w modelach embeddingowych mają tendencję do mieszania się
Należy go używać tam, gdzie precyzja w odniesieniu do konkretnych terminów jest ważniejsza niż tolerancja na parafrazę: w dziedzinie prawa, medycyny, wyszukiwania kodów i logów. Jest tani i szybki, nie wymaga szkolenia ani karty graficznej. Jego słabością jest brak jakiegokolwiek zrozumienia semantycznego, więc „samochód” i „automobil” wydają mu się niespowiązane.
2. Gęste odzyskiwanie wektorów: dopasowywanie znaczeń
Dense retrieval przekształca zapytania i dokumenty w wektory embeddingowe o setkach do tysięcy wymiarów, bez zer, wytwarzane przez kodera neuronalnego. Podobieństwo jest mierzone za pomocą iloczynu kosinusowego lub iloczynu skalarnego, a przybliżony indeks najbliższych sąsiadów, takich jak HNSW lub IVF, znajduje najbliższe elementy.
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]
Uchwytuje ono podobieństwo semantyczne: „rezygnacja kierownictwa” znajduje „prezes ustąpił”, mimo braku wspólnych słów. Ludzie często nazywają to wyszukiwaniem semantycznym, chociaż ściśle rzecz biorąc mechanizmem jest gęste wyszukiwanie, a wynikiem dopasowanie semantyczne.
Należy go stosować przy zapytaniach konwersacyjnych, pytaniach parafrazowanych, wyszukiwaniu międzyjęzykowym lub wtedy, gdy użytkownicy nie będą używać dokładnej terminologii z dokumentów. Może on zawodzić przy rzadkich terminach, dokładnych kodach i liczbach, ponieważ przestrzeń embeddingów wygładza szczegóły, które BM25 zachowuje.
3. Przepisywanie zapytań i HyDE: naprawa zapytania najpierw
Czasami sam mechanizm wyszukiwania jest w porządku, a problem leży w zapytaniu. Użytkownicy wpisują krótkie, niejasne lub dziwnie sformułowane pytania, podczas gdy zarówno BM25, jak i gęste wyszukiwanie działają najlepiej na dobrze sformatowanym, bogatym w informacje tekście.
Przepisywanie zapytania polega na tym, że model językowy rozszerza lub precyzuje pytanie przed wyszukiwaniem, przekształcając „jak szybko” w dokładne pytanie o to, ilu dokumentów na sekundę może obsłużyć pipeline indeksowania X w warunkach maksymalnej wydajności.
HyDE (Hypothetical Document Embeddings) idzie jeszcze dalej. Zamiast embedować samo zapytanie, od modelu językowego wymaga się wymyślenia wiarygodnej odpowiedzi, a ta sfabrykowana odpowiedź jest następnie embedowana i wykorzystywana do znajdowania rzeczywistych dokumentów. Intuicja polega na tym, że sfabrykowana odpowiedź często znajduje się w przestrzeni embedowania bliżej prawdziwych odpowiedzi niż samo pytanie.
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()
Krótkie lub niejasne zapytania należą do najczęstszych przyczyn słabej skuteczności wyszukiwania, ponieważ takie zapytania po prostu nie zawierają wystarczająco silnego sygnału. Jest to szczególnie pomocne w interfejsach typu czat oraz w dziedzinach, gdzie słownictwo użytkowników różni się od słownictwa zawartego w korpusie. Kosztem jest dodatkowy wywołanie modelu LLM przed rozpoczęciem wyszukiwania, co powoduje większą opóźnienie i wyższe koszty, a słaba hipotetyczna odpowiedź może wpłynąć negatywnie na wyniki wyszukiwania.
4. Wyszukiwanie hybrydowe: wykorzystanie obu metod
BM25 oraz metody intensywnego wyszukiwania zawodzą na przeciwnych krańcach spektrum. Wyszukiwanie hybrydowe uruchamia obie metody równolegle i łączy listy wyników, dzięki czemu w jednej rundzie uda się odnaleźć zarówno dokładne terminy, jak i ich parafrazy. Problem polega na tym, że oceny BM25 oraz podobieństwa kosinowe odnoszą się do różnych skali, więc ich średnienie nie jest skuteczne. To właśnie ma za zadanie fuzja rang wzajemnych.
5. Fuzja rang wzajemnych: łączenie rang, a nie ocen
RRF całkowicie unika normalizacji wyników, łącząc je na podstawie pozycji w rankingu i ignorując surowe wyniki.
RRF_score(doc) = Σ 1 / (k + rank_r(doc)) for each ranker r
Stała k, zwykle równa 60, łagodzi ten efekt, dzięki czemu pojedyncze pierwsze miejsce nie może dominować.
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])
Dokumenty, które otrzymują wysokie oceny od kilku systemów wyszukiwania, naturalnie trafiają na czoło, bez konieczności kalibracji skali ocen. To standardowy sposób łączenia wyników hybrydowych i nadaje się do każdego przypadku, gdy łączy się dwie lub więcej list rankingowych z różnych systemów wyszukiwania, takich jak BM25 w połączeniu z modelem gęstym lub kilka modeli embeddingowych.
6. Maksymalna marginalna istotność: eliminacja redundancji
Doskonały ranking może nadal być bezużyteczny, jeśli pierwsze pięć elementów mówi o tym samym. MMR ponownie rankuje elementy pod kątem zarówno istotności, jak i różnorodności.
MMR = λ · Sim(doc, query) − (1 − λ) · max(Sim(doc, selected))
Parametr λ określa równowagę. Przy λ = 1 uzyskujemy zwykły algorytm top-k, natomiast przy λ = 0 maksymalizowana jest różnorodność, niezależnie od istotności.
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]
Bez MMR można przeznaczyć całe okno kontekstowe na pięć niemal identycznych fragmentów zamiast na pięć fragmentów obejmujących różne aspekty odpowiedzi. Jest to korzystne w korpusach z nakładającym się lub powtarzającym się treścią, takich jak strony FAQ, dokumentacja produktowa z szablonami czy jakikolwiek zbiór niemal identycznych tekstów.
7. Ponowna klasyfikacja za pomocą cross-encodera: klucz do dokładności
Wszystko, co dotychczas omówiono, należy do świata bi-encoderów, w których zapytania i dokumenty są embedowane oddzielnie, a następnie porównywane. Cross-encoder wprowadza zarówno zapytanie, jak i kandydatów do jednego transformera, dzięki czemu model może badać oba teksty. Dzięki temu uzyskuje się znacznie dokładniejsze oceny istotności w porównaniu z porównywaniem wcześniej obliczonych wektorów.
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)]
Kodery krzyżowe regularnie przewyższają kodery dwukierunkowe pod względem oceny trafności wyników, ponieważ rozważają jednocześnie zapytanie i dokument. Należy je użyć jako końcowej korekty na małym zestawie kandydatów, zazwyczaj najlepszych 20 do 50 po fuzji lub metodzie MMR, a nigdy na całym korpusie, gdyż każda para dokumentów wymaga osobnego przetworzenia. Sam wywołanie modelu LLM stanowi największy dodatkowy opóźnienie na tej liście, dlatego znajduje się na końcu procesu, a nie na początku.
8. Czas odpowiedzi: koszt całego tego procesu
Każda z powyższych technik poprawia jakość wyszukiwania, ale żadna z nich nie jest bezpłatna. Każda z nich powoduje opóźnienie, a całkowity czas od wysłania zapytania do otrzymania ostatecznej odpowiedzi decyduje o tym, czy starannie dostrojony proces może być używany w produkcji. Typowe koszty na każdym etapie:
- Przepisywanie zapytania lub HyDE, jeśli jest używane: około 200 ms do 1 s, dodatkowy wywołanie LLM przed rozpoczęciem wyszukiwania.
- Wyszukiwanie w BM25: około 5 do 20 ms, przy użyciu taniego odwróconego indeksu w pamięci.
- Wyszukiwanie wektorowe gęste (ANN): około 10 do 50 ms, w zależności od rozmiaru i typu indeksu – HNSW lub płaski.
- Fuzja RRF: 1 do 5 ms, prosta arytmetyka na listach rankingowych.
- Ponowne rankowanie MMR: około 20 do 100 ms, czas rośnie wraz z liczbą kandydatów, ponieważ jest procesem iteracyjnym i parowym.
- Ponowne rankowanie za pomocą cross-encodera, jeśli jest używane: około 50 do 300 ms, często największy pojedynczy koszt przed generowaniem wyników.
- Generowanie przez LLM: od około 500 ms do kilku sekund, zazwyczaj stanowi największy udział w całkowitym czasie.
Aby utrzymać ten całkowity czas w ryzach:
- Uruchamiaj BM25 oraz metodę dense retrieval równolegle – nie są one od siebie zależne.
- Zastosuj MMR do małej grupy wyników, na przykład do 20–30 najlepszych po procesie RRF, ponieważ porównania podwójne mają duży wpływ na wydajność.
- Przechowuj embeddingi dla często zadawanych lub powtarzających się zapytań w pamięci cache.
- Dodaj cross-encoder tylko wtedy, gdy wzrost dokładności wyraźnie usprawiedliwia spowolnienie.
- Śledź czas odpowiedzi jako ważną miarę obok jakości odpowiedzi, takiej jak wierność lub recall@k. System o o 2 procent wyższej dokładności, ale trzy razy wolniejszy, może nie być wart wdrożenia.
Jak wszystko się łączy
Produkcyjny hybrydowy pipeline wyszukiwania zazwyczaj działa w następujący sposób:
- Przed wyszukiwaniem opcjonalnie przeprowadza się przepisywanie zapytania lub używa HyDE w celu uporządkowania niejasnego zapytania, co może powodować opóźnienie.
- BM25 wykrywa dokładne terminy.
- Metoda dense retrieval bierze pod uwagę znaczenie.
- RRF łączy obie listy bez konieczności normalizacji wyników.
- MMR redukuje połączoną listę do zróżnicowanego, niepowtarzalnego zbioru kandydatów.
- Kodownik krzyżowy przeprowadza ostateczną, wysokodokładną analizę tego mniejszego zbioru.
- Czas odpowiedzi jest wskaźnikiem pokazującym, czy całość nadaje się do użycia.
Szybki przewodnik decyzyjny
- Zapytania krótkie, niejasne lub poza słownictwem? Przepisz je lub spróbuj HyDE.
- Dokładna dopasowanie kodów, identyfikatorów i rzadkich terminów? BM25.
- Tolerancja na parafrazowanie? Gęste wyszukiwanie.
- Oba naraz? Połącz za pomocą RRF.
- Powtarzające się fragmenty w kontekście? MMR.
- Najwyższa istotność w małym zbiorze końcowym? Ponowne sortowanie za pomocą kodownika krzyżowego.
- Dokładne, ale wolne? Zmierz każdy etap przed ślepą optymalizacją.
Żadna pojedyncza technika nie jest rozwiązaniem idealnym. Najlepsze systemy produkcyjne łączą je ze sobą, dostosowując je na podstawie rzeczywistych logów zapytań, a nie intuicji, przy czym optymalny stosunek tych technik wciąż w dużej mierze zależy od korpusu danych oraz dostępnego budżetu opóźnień.