Hibrydowe odzyskiwanie danych z RAG przy użyciu pgvector, BM25 oraz ponownego rankowania za pomocą Cross-Encodera
Dowiedz się, dlaczego czyste wyszukiwanie wektorowe pomija numery części i kody błędów, oraz jak połączyć pgvector, BM25 i ponowne sortowanie w LangChain w celu dokładnego odzyskiwania danych RAG.
Prototyp generacji wzbogaconej o wyszukiwanie (RAG), zbudowany na zwykłym wyszukiwaniu wektorowym, zazwyczaj robi wrażenie podczas demonstracji, ale potem rozczarowuje prawdziwych użytkowników. Zadaj mu pytanie dotyczące harmonogramu konserwacji określonego modelu pompy, a otrzymasz ogólne porady dotyczące pomp; jeśli szukasz dokładnego kodu błędu, odpowiednia instrukcja naprawy nigdy się nie pojawia. Ten przewodnik wyjaśnia, dlaczego intensywne wyszukiwanie zawodzi przy dokładnych identyfikatorach, oraz pokazuje, jak to naprawić za pomocą hybrydowego podejścia: pgvector do wyszukiwania semantycznego, BM25 do dopasowywania słów kluczowych oraz narzędzie do ponownego rankowania, które decyduje, które fragmenty trafią do modelu LLM.
Dlaczego wyszukiwanie wektorowe pomija dokładne identyfikatory
Embeddingi doskonale radzą sobie z przekazywaniem znaczenia. Zapytanie dotyczące „automobile” trafia w pobliże dokumentów o „cars” i „vehicles”, ponieważ model embeddingu umieszcza powiązane koncepcje blisko siebie w przestrzeni wysokowymiarowej. Ta sama cecha jest jednak jego słabością – podobieństwo dotyczy semantycznej bliskości, a nie dokładnych sekwencji znaków.
Gdy użytkownik szuka numeru części takiego jak TX-99402 lub kodu błędu takiego jak E-404, embedding tej sekwencji może znajdować się bardzo blisko TX-99401 lub ogólnych tekstów pomocnych przy rozwiązywaniu problemów. System wyszukiwania zwraca dokumenty, które są „o tym samym”, zamiast tego, który zawiera dokładnie wpisany przez użytkownika token. W przypadku instrukcji technicznych, katalogów produktów i baz wiedzy obsługi, gdzie identyfikatory niosą większość znaczenia, jest to dominujący sposób awarii.
Hybrydowe wyszukiwanie: gęste i rzadkie równolegle
Rozwiązaniem jest uruchomienie dwóch uzupełniających się mechanizmów wyszukiwania i połączenie ich wyników:
- Gęste wyszukiwanie (wyszukiwanie wektorowe) uwzględnia kontekst i znaczenie. Zamiast używać oddzielnej bazy danych wektorowej, można przechowywać embeddingi w PostgreSQL za pomocą rozszerzenia
pgvector. Wiele aplikacji już działa na Postgresie, więc dodanie kolumny wektorowej pozwala utrzymać prostą architekturę oraz korzystać z znajomych funkcji kopii zapasowych, kontroli dostępu i transakcji. - Rzadkie wyszukiwanie (wyszukiwanie według słów kluczowych) znajduje dokładne dopasowania, akronimy oraz żargon specjalistyczny. Standardowym algorytmem jest BM25 – długo stosowana funkcja rankowania, która ocenia dokumenty na podstawie częstotliwości występowania terminów z zapytania, uwzględniając jednocześnie rzadkość tych terminów w całym korpusie oraz długość dokumentu.
Korzystny model mentalny: wyszukiwanie wektorowe znajduje odpowiednią okolicę, a BM25 – dokładny numer domu. Bierze się najlepsze wyniki z każdego z nich i łączy je. Aby dowiedzieć się więcej na temat sytuacji, w której każde podejście ma przewagę, zapoznaj się z naszym artykułem o hybrydowym wyszukiwaniu wiedzy technicznej.
Dlaczego połączone wyniki wymagają ponownego rankowania
Hybrydowe wyszukiwanie stwarza natychmiastowy problem. Mamy teraz dwie listy uszeregowane według określonych kryteriów, których wyniki nie są porównywalne. Wynik BM25 zależy od częstotliwości występowania terminów i nie ma górnej granicy, natomiast podobieństwo wektorowe opiera się na odległości kosinowej w zupełnie innej skali. Wynik semantyczny 0,82 nie jest lepszy ani gorszy od wyniku BM25 14,5; sortowanie zespołu wyników według surowego wyniku jest bezsensowne.
Reranker rozwiązuje ten problem, ignorując początkowe oceny. Jest to odrębny model, zazwyczaj cross-encoder, który czyta zapytanie i dokument kandydat razem i wyprowadza pojedynczą ocenę trafności dla tego zestawu. Ponieważ widzi oba teksty jednocześnie, może ocenić ich trafność znacznie dokładniej niż poprzez porównywanie dwóch niezależnie obliczonych embeddingów.
Przebieg procesu jest następujący:
- Pobierz 10 kandydatów z każdego narzędzia wyszukiwania, pgvector i BM25.
- Zgrupuj je, uzyskując maksymalnie 20 fragmentów.
- Oceń każdy fragment pod kątem zapytania za pomocą rerankera.
- Zachowaj 3 najlepsze i przekaż je jedynie do LLM.
Mniej, lepszych fragmentów oznacza również krótszy prompt, mniej szumu do zignorowania przez model oraz niższe koszty tokenów.
Koszt opóźnienia
Kodery krzyżowe są drogie. Przetwarzanie 20 dokumentów dodaje znaczną ilość czasu do każdej prośby, a w API do czatów strumieniowych (na przykład takim zbudowanym z FastAPI) opóźnia dostarczenie pierwszego tokena. Pomiierz ten etap oddzielnie w swoim monitorowaniu opóźnień. Zysk dokładności zazwyczaj jest tego warty, ale dostosuj liczbę kandydatów do swojego budżetu; naszy artykuł na temat tego, dlaczego reranking musi zasłużyć na swoje opóźnienie, szczegółowo omawia ten kompromis.
Wdrożenie pipeline’a za pomocą LangChain
LangChain dostarcza elementy budulcowe dla każdej części, dzięki czemu cały pipeline mieści się w dwóch krótkich funkcjach w Pythonie. Poniższe przykłady mają charakter koncepcyjny; dostosuj ciąg połączenia, modele oraz ścieżki plików do swojego środowiska.
Przyjmowanie danych: dzielenie na fragmenty, embedowanie i indeksowanie dwukrotnie
Funkcja pobierania danych ładuje plik tekstowy, dzieli go na fragmenty po 1000 znaków z 100-znakowym nakładaniem się, a następnie indeksuje te same fragmenty na dwa sposoby. Po pierwsze, wstawia je za pomocą modelu OpenAI text-embedding-3-small i przechowuje w kolekcji pgvector za pomocą PGVector.from_documents. Po drugie, stosuje do tych fragmentów BM25Retriever i zapisuje je na dysku za pomocą pickle, ponieważ BM25 buduje swój indeks w pamięci.
import pickle
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_postgres.vectorstores import PGVector
from langchain_community.retrievers import BM25Retriever
CONNECTION_STRING = "postgresql+psycopg://user:password@localhost:5432/mydb"
COLLECTION_NAME = "hybrid_docs"
def ingest_documents(file_path: str):
# 1. Load and chunk the document
loader = TextLoader(file_path)
docs = loader.load()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
chunks = text_splitter.split_documents(docs)
# 2. Store dense embeddings in pgvector
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
PGVector.from_documents(
embedding=embeddings,
documents=chunks,
collection_name=COLLECTION_NAME,
connection=CONNECTION_STRING,
)
# 3. Fit and save the BM25 sparse retriever
bm25_retriever = BM25Retriever.from_documents(chunks)
with open("bm25_retriever.pkl", "wb") as f:
pickle.dump(bm25_retriever, f)
print(f"Successfully ingested {len(chunks)} chunks.")
# Example usage:
# ingest_documents("technical_manual.txt")
Rzeczy, które należy mieć na uwadze:
- Indeks BM25 stanowi tylko moment obrazowy. Gdy dokumenty się zmieniają, trzeba go ponownie utworzyć i zapisać, w przeciwnym razie straci synchronizację z magazynem wektorowym.
- Tylko otwieraj pliki, które sam stworzyłeś. Otwieranie pliku w formacie pickle uruchamia kod, więc sfałszowany plik stanowi zagrożenie dla bezpieczeństwa.
Pobieranie wyników: zespół metod, a następnie ponowny ranking
Funkcja pobierania wyników odbudowuje obie metody i łączy je ze sobą. Metoda pgvector zwraca 10 najlepszych semantycznych dopasowań (k=10), a metoda BM25 również ma ustawione zwracanie 10 wyników. EnsembleRetriever łączy je ze sobą, przydzielając im równe wagi wynoszące 0.5. Kompresor CohereRerank z ustawieniem top_n=3 otacza ten zespół metod wewnątrz ContextualCompressionRetriever, dzięki czemu każde zapytanie przechodzi przez proces pobierania, łączenia i ponownego rankingu w ramach jednej wywołania invoke.
import pickle
from langchain_openai import OpenAIEmbeddings
from langchain_postgres.vectorstores import PGVector
from langchain.retrievers import EnsembleRetriever, ContextualCompressionRetriever
from langchain_cohere import CohereRerank
CONNECTION_STRING = "postgresql+psycopg://user:password@localhost:5432/mydb"
COLLECTION_NAME = "hybrid_docs"
def setup_hybrid_retriever():
# 1. Initialize Vector Retriever
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = PGVector(
connection=CONNECTION_STRING,
embeddings=embeddings,
collection_name=COLLECTION_NAME,
)
# Fetch top 10 semantic matches
pgvector_retriever = vectorstore.as_retriever(search_kwargs={"k": 10})
# 2. Load Keyword Retriever (BM25)
with open("bm25_retriever.pkl", "rb") as f:
bm25_retriever = pickle.load(f)
# Fetch top 10 exact keyword matches
bm25_retriever.k = 10
# 3. Merge pools with EnsembleRetriever (50/50 weighting)
hybrid_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, pgvector_retriever],
weights=[0.5, 0.5]
)
# 4. Rerank the combined 20 chunks to output the absolute top 3
reranker = CohereRerank(cohere_api_key="YOUR_COHERE_API_KEY", top_n=3)
advanced_retriever = ContextualCompressionRetriever(
base_compressor=reranker,
base_retriever=hybrid_retriever
)
return advanced_retriever
def query_system(query: str):
retriever = setup_hybrid_retriever()
best_docs = retriever.invoke(query)
for i, doc in enumerate(best_docs):
print(f"\n--- Result {i+1} ---")
print(doc.page_content)
# Example usage:
# query_system("What is the warranty period for the TX-99402 sensor?")
Kilka szczegółów, które łatwo przeoczyć:
EnsembleRetrievernie dodaje surowych ocen. Łączy listy według rankingu za pomocą ważonej fuzji wzajemnego rankingu, co pozwala uniknąć opisanego powyżej niezgodności skalowania. Usuwa również duplikaty, więc narzędzie do ponownego rankingu może otrzymać mniej niż 20 fragmentów, gdy oba mechanizmy wyszukiwania znajdą ten sam.- Nigdy nie umieszczaj klucza API w kodzie źródłowym. Odczytuj klucz Cohere z zmiennej środowiskowej lub menedżera sekretów.
setup_hybrid_retriever()jest wykonywany przy każdej zapytaniu, ponownie łącząc się z Postgres i dekompresując BM25 za każdym razem. W rzeczywistym serwisie należy utworzyć mechanizm wyszukiwania raz podczas uruchamiania i używać go ponownie.
EnsembleRetriever i ContextualCompressionRetriever mogą znajdować się w innym pakiecie w Twojej wersji. Sprawdź aktualną dokumentację LangChain, jeśli wystąpi błąd importu.Główne wnioski
- Czyste wyszukiwanie wektorowe jest słabe w przypadku dokładnych tokenów, takich jak numery części, SKU i kody błędów; BM25 wypełnia tę lukę.
- pgvector umożliwia dodanie zaawansowanego wyszukiwania do istniejącej infrastruktury PostgreSQL bez konieczności używania oddzielnego bazę danych wektorowej.
- Wyniki z różnych mechanizmów wyszukiwania nie są porównywalne, dlatego należy je połączyć według rankingu i zlecić mechanizmowi cross-encoder reranker ustalenie ostatecznej kolejności.
- Reranking poprawia precyzję, ale zwiększa opóźnienie; starannie dobieraj rozmiar grupy kandydatów i monitoruj ją.
- Traktuj indeks BM25 jako artefakt budowy, który musi być aktualizowany zgodnie z danymi, oraz unikaj przechowywania danych uwierzytelniających w kodzie.
Hybrydowe wyszukiwanie nie gwarantuje doskonałych odpowiedzi, ale eliminuje najczęstą przyczynę, dla której systemy RAG w produkcji zwracają wiarygodny, lecz błędny kontekst.
Literatura pokrewna
- Projektowanie czteropoziomowej pamięci agenta z użyciem LangGraph i Amazon Bedrock — Dowiedz się, jak zapewnić agentom LLM funkcjonalną pamięć epizodyczną, semantyczną i proceduralną na platformach Bedrock i LangGraph oraz jak chronić ją przed zatruwaniem, wyciekami danych osobowych i problemami związanych z użytkownikami.
- LangChain 1.x w praktyce: łańcuchy, RAG, narzędzia i agenci lokalnie — Naucz się budować łańcuchy, systemy generacji wzbogaconej o wyszukiwanie danych, narzędzia oraz agenty RAG przy użyciu LangChain 1.x z darmowej lokalnej instalacji Ollama – nie są potrzebne klucze API.
- Poza Top-K: Progi relewności, hybrydowe wyszukiwanie i preranking w RAG — Dowiedz się, dlaczego baza danych wektorowych w połączeniu z LLM nie stanowi gotowego systemu RAG, oraz jak dzielenie na fragmenty, progi podobieństwa, hybrydowe wyszukiwanie, preranking i ocena mogą zamknąć tę lukę.