Strona główna / Artykuły / Hibrydowe odzyskiwanie danych z RAG przy użyciu pgvector, BM25 oraz ponownego rankowania za pomocą Cross-Encodera

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.

1524 słów

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:

  1. 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.
  2. 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:

  1. Pobierz 10 kandydatów z każdego narzędzia wyszukiwania, pgvector i BM25.
  2. Zgrupuj je, uzyskując maksymalnie 20 fragmentów.
  3. Oceń każdy fragment pod kątem zapytania za pomocą rerankera.
  4. 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.
  • Łańcuch połączenia zawiera dane uwierzytelniające; należy go ładować z konfiguracji, zamiast wpisywać je ręcznie.
  • Jeśli chcesz zachować również wyszukiwanie według słów kluczowych w bazie danych, wbudowane w PostgreSQL wyszukiwanie pełnego tekstu stanowi alternatywę dla indeksu BM25 działającego wewnątrz procesu, oferując inne zachowanie rankingowe.
  • 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ć:

    • EnsembleRetriever nie 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.
  • LangChain przearanżował swoje pakiety w poszczególnych wersjach, więc klasy takie jak 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