За межамі top-K: BM25, гібрыдны пошук, пераранжаванне і MMR у RAG
За межамі top-K у RAG: BM25 для дакладных тэрмінаў, гібрыдны пошук з RRF, пераранжаванне крос-энкодэрам і MMR для разнастайнасці, аб’яднаныя ў адзін канвеер.
Крыніцы: Retrieval Strategies in RAG: Beyond Basic Similarity Search
Першыя сістэмы RAG, якія ствараюць большасць распрацоўшчыкаў, адпавядаюць аднаму прыему: ўбудовваюць дакументы, размешчаюць іх у векторной базе дадзеных, а калі прыходзіць запіт, беруць найбліжэйшыя фрагменты і перэдаюць іх LLM. Гэта працюе, пакуль не перастае. Пользоватары пачынаюць скаржыцца, што пропускаюць очвідныя адпаведзі. У дакуменце чыстая форма містыць неабходную інфармацыю, але сістэма не можа яе знайсці, або найболей прыдатны рэзультат на першым месцы на першы погляд падобны запыту, але не ўсунець потрэбы карыстальніка. Звычны пошук за сэмплярнасцю — гарная пачатковая точка з рэальнымі мерыямі, і калі яны стануць яснымі, лепшыя стратэгіі пошуку іх усунуць. Хуткая інфармацыя пра прычыны неякосці, пра тое, як гібрыдны пошук і пераранжаванне гэта рашаюць, і пра тое, як склікці всі элементы ў адну сістэму.
Пошук за сэмплярнасцю, кароткая інфармацыя
У простай наладзе запытка карыстальніка ператвараецца ў эмбеддынг — спіс цяжэй, якія выражаюць яго значэнне. Кожны фрагмент у хранільні вектараў таксама мае свой эмбеддынг. Сістэма знаходзі тые фрагменты, чыя эмбеддынгі знаходзяцца найбліжэй да эмбеддынга запыткі, што вылічаецца за дапамою косай схожасці або скалярнага добутку, і вяртае K найлепшых рэзультатаў.
# Basic similarity search — what most RAG systems do
results = vector_store.similarity_search(query, k=3)
Для загальнага сэмантычнага падабрання ён добраўка. Запыт пра тое, як працуе фотасынтэз, пакажае інфармацыю пра тое, як растэнні ператвараюць сонячнае святло ў энергію, нават калі формулаванне запыту разныя. Але ён мае слабыя месцы.
Дзе ён не дастаеяе
Точныя ключовыя словы застаюцца незаўважанымі. Сэмантычны пошук фіксуе значэнне, а не буквальныя словы, што зазвычай ёсць перавагай. Аднак калі хтось шукае конкрэтны тэрмін, напрыклад назву продукту, абрывку чы ўзначненне адной чалавека, эмбеддынг можа не падабрацца дастатнь чыста. Спытайцеся: якшо корыстнік запытаецца пра архітэктуру модэлі BERT, існуе фрагмент, які пазначае, што BERT выкарыстоўвае двунаправленыя трансфармэры, але шырэйшы фрагмент пра архітэктуру мовных модэляў можа мець вышэйшую прыорытэтнасць, таму што ён сэмантычна шырэйшы.
Высокая зачытка не адначына ся з найлепшай адказом. Парадоксамасцьві паказывае, наскількі два тэксты адносуюцца адзін да другога, а не наскількі адзін з іх корыстны для адказу на запытку іншага. Фрагмент, у яком упомінаецца, што сістэмы RAG ўдобныя для адказу на запыткі, дужа блізкі да запыткі «Што такое RAG?», тады як фрагмент, у яком знаходзіцца сама вызначэнне, ўжо корыстнейшы.
Топавыя рэзультаты таксама можуць быць зайвымі. Рэальныя дакументы павтараюцца ў разных фрагментах, таму тры самыя топавыя рэзультаты можуць гаварыць практычна адно і тое ж, выкорыстоўваючы прастор кантэксту на дублікатны матэрыял.
BM25 для пошуку ключоўых слоў
BM25 — гэта класычны алгорытм адзысквітання інфармацыі, які выкорыстоўваецца ў традыцыйных пошуковых сістэмах. Ён паўтарае дакументы за дапамою точных ключоўых слов, пры чым кожнае з іх прызначаецца павышанай вагою залежна ад таго, насколькі яна рэдкая ў всім корпусе, і ён не выкорыстоўвае эмбеддынгі, а толькі чысты тэкст. Пошук фразы „FAISS index“ вяртае фрагменты, якія буквальна маюць оба словы, пры чым рэдкае слова мае большую вагу, чым распашчастае.
from langchain_community.retrievers import BM25Retriever
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
# Load and split documents
loader = TextLoader("knowledge_base.txt")
documents = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(documents)
# Create BM25 retriever
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 3
# Search
results = bm25_retriever.invoke("FAISS vector index")
for doc in results:
print(doc.page_content[:200])
Ён функцыонуе эфкарэй з конкрэтнымі назвамі продуктав, іменамі або тэхнічнымі тэрмінамі, з кораткімі, тачнымі запыткамі, а таксама там, дзе тачная формулірацыя мае большое значэнне чым значэнне самага тэксту. Ён мае труднасці, калі корыстнік формулюе запыткі інакш, чым у дасяжных дакументах (за дапамою сінонімав, парафраз), калі запыткі дужа дзялгія, або калі значэнне пераважае буквальныя словы.
Гібрыдны пошук
У гібрыдным пошуку адночасова выконваецца семантычны пошук і пошук за дапамою методу BM25. Семантычны пошук разбіраецца з значэннем і сінонімамі, а BM25 — з тачнымі ключовымі словамі, таму разам яны пакрываюць сітуацыі, якія не моглі б быць адкрытыя кожным з яўных методаў. Две спісы рэзультатаў з’едынаюцца за дапамою тэхнікі Reciprocal Rank Fusion (RRF), якая з’едынае спісы з рангаванням у адны, не выклікаючы патрэбы, каб ўсія ў ямі оцэнкі былі на аднай шкале.
from langchain_community.retrievers import BM25Retriever
from langchain_google_genai import GoogleGenerativeAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain.retrievers import EnsembleRetriever
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import TextLoader
#Load and split
loader = TextLoader("knowledge_base.txt")
documents = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(documents)
#Retriever 1 — Semantic (FAISS)
embeddings = GoogleGenerativeAIEmbeddings(
model="models/embedding-001",
google_api_key="YOUR_KEY"
)
vector_store = FAISS.from_documents(chunks, embeddings)
semantic_retriever = vector_store.as_retriever(search_kwargs={"k": 5})
#Retriever 2 — Keyword (BM25)
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 5
#Combine both — Hybrid Search
hybrid_retriever = EnsembleRetriever(
retrievers=[semantic_retriever, bm25_retriever],
weights=[0.6, 0.4] # 60% semantic, 40% keyword
)
#Search
results = hybrid_retriever.invoke("What is FAISS and how does it store vectors?")
print(f"Retrieved {len(results)} chunks")
for i, doc in enumerate(results):
print(f"\nChunk {i+1}: {doc.page_content[:150]}")
Параметр weights задае, насколькі кожны з алгорытмаў аднаходжэння дакументаў вялікі вклад даўа. Няшто большая акцэнтаванасць на семантычным пошуку падходзіць для большай часткі случаў, аднак правы баланс залежыць ад вашых дадзенаў. Выберыце гібрыдны падход, калі дакументы маюць тэхнічныя тэрміны, назвы продуктав або власныя імены, калі корыстувачы выкорыстоўваюць як канверсацыйныя, так і запыты на аснове ключоўых слоў, або калі вы хочаце, каб падтрымка ключоўых слоў служыла запасным варыянтам пасля семантычнага розумення.
Пераранжаванне
Працэс пераранжавання адбываецца у два этапы. Спачатку адбываецца шырокі пошук, прагледваюцца 20 або 30 лепшых рэзультатаў за дапамогою алгорытма аднаходжэння дакументаў, што ёсць дашчацай і шырока. Потым гэтыя кандыдаты праходзяць праз спецыяльную мадэль пераранжавання, якая оцінюе кожны з іх за тое, насколькі добра ён адпавядае на запыт, і застаецца толькі 3-5 найкрашчых рэзультатаў.
Прычына ў тым, што вектарная сэмліярнасць — гэта быстрая апрыксія рэлевантнасці, а прыем па пераранкаванню ўсё працюе медленней, але ўзніклае рэзультат ўсё болей тачны. Спачатку звужэнне за дапамою сэмліярнасці, а пасля пераранкаванне, дазволяе досягнуць як шырокаспектнасці, так і тачнасці.
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain.retrievers import ContextualCompressionRetriever
#Base retriever — fetch top 20 candidates
base_retriever = vector_store.as_retriever(search_kwargs={"k": 20})
#Reranker model
reranker_model = HuggingFaceCrossEncoder(
model_name="BAAI/bge-reranker-base"
)
#Reranker compressor — keeps only top 3 after reranking
reranker = CrossEncoderReranker(model=reranker_model, top_n=3)
#Combine base retriever + reranker
reranking_retriever = ContextualCompressionRetriever(
base_compressor=reranker,
base_retriever=base_retriever
)
#Search — fetches 20, reranks, returns top 3
results = reranking_retriever.invoke("How does FAISS perform similarity search?")
print(f"Final chunks after reranking: {len(results)}")
for i, doc in enumerate(results):
print(f"\nTop {i+1}: {doc.page_content[:200]}")
Крос-кодэр-рэранкер чытае запіт і адзін кантэкстны фрагмент разам як пару і выявляе, насколькі самэўажлівы ўсё-такі гэты фрагмент для конкрэтнага запіта. Метаданы сэмабільнасці вектара аналізуюць кожны тэкст окалічна і ніколі не ацэнююць ўзаінтэрэсаванне між ямі безпосерэдньа. Выбірайце метод рэранкінгу, калі важна тачнасць і неправільныя адказы маюць вялікія наследкі, калі вы адпавядаеце на запыткі на адпаведных дакументах, або калі якасць адказу безпасовершна вплывае на корыстувачаў.
Максымальная маргінальная ужытковасць
Метод MMR борэцца з зайвасцю. У замест на простае выбіранне найбліжэйшых фрагментаў, ён выбірае тыя, якія ўжоць ужытковыя і разнараднія, так што адна і тая ж інфармацыя не вяртаецца колькі разоў.
# Standard similarity search — might return redundant chunks
standard_results = vector_store.similarity_search(query, k=3)
# MMR search — returns relevant AND diverse chunks
mmr_results = vector_store.max_marginal_relevance_search(
query,
k=3, # final number of chunks to return
fetch_k=20, # candidates to consider before selecting diverse ones
lambda_mult=0.5 # 0 = max diversity, 1 = max relevance
)
Параметр lambda_mult задае баланс між рэлевантнасцю і разнароднасцю. Значэнне 1.0 означае чыстую рэлевантнасць, ідэнтычную зяманайшам пошуку за сэмліннасцю. Значэнне 0.0 означае чыстую разнароднасць, калі фрагменты ўсё болей розныя між сабою. Значэнне 0.5 балансуе гэтыя два падходы і зазвычай ўжо ёсць хорашым пачатковым настройкам. Метод MMR ёсць корыстным, калі дакументы ёсць павтаральнымі, калі трэба ахватыць калькі аспекта тэмы, або калі вікна кантэксту занадта маленькія для павтаральных фрагментаў.
Полны працэс
Як толькі гэтыя элементы з’едынаюцца, гібрыдны метод пошуку розкрывае шырокі спектар можлівасцей, перыярхатаванне выбірае найболей рэлевантныя фрагменты, а цэлы ланцуг RAG стварае адпаведную адпаведзь.
from langchain_google_genai import ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_community.retrievers import BM25Retriever
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
from langchain.retrievers import EnsembleRetriever, ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import TextLoader
# Setup
llm = ChatGoogleGenerativeAI(model="gemini-1.5-flash", google_api_key="YOUR_KEY")
embeddings = GoogleGenerativeAIEmbeddings(model="models/embedding-001", google_api_key="YOUR_KEY")
# Load and split documents
loader = TextLoader("your_knowledge_base.txt")
documents = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(documents)
# Build retrievers
vector_store = FAISS.from_documents(chunks, embeddings)
semantic_retriever = vector_store.as_retriever(search_kwargs={"k": 10})
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 10
# Hybrid retriever
hybrid_retriever = EnsembleRetriever(
retrievers=[semantic_retriever, bm25_retriever],
weights=[0.6, 0.4]
)
# Add reranking on top
reranker = CrossEncoderReranker(
model=HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base"),
top_n=3
)
final_retriever = ContextualCompressionRetriever(
base_compressor=reranker,
base_retriever=hybrid_retriever
)
# RAG prompt
prompt = ChatPromptTemplate.from_messages([
("system", """Answer the question using only the context below.
If the answer is not in the context, say "I don't have that information."
Context: {context}"""),
("human", "{question}")
])
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
# Complete chain
rag_chain = (
{"context": final_retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# Use it
answer = rag_chain.invoke("Your question here")
print(answer)
Якасць выкарыстоўвання дадзеных выходзіць значна лепшая, чым проста пошук за аднаковасцю.
Выбір стратэгіі
Короткі практычны паказчык супарабатоўвае сітуацыі з стратэгіямі:
Just starting out / simple use case?
→ Basic similarity search is fine
Documents have specific technical terms or product names?
→ Add BM25 → use Hybrid Search
Answer quality is critical, wrong answers are costly?
→ Add Reranking on top of any retriever
Documents have lots of repeated content?
→ Use MMR instead of standard similarity search
Production system with high quality requirements?
→ Hybrid Search + Reranking together
Не трэба занадта складнаць сістэму з самага пачатку. Пачніце з базовага пошуку за сэмвалентнасцю, дадзіце можлівасць гібрыднага пошуку, калі пазірэце, што ключавыя словы не знаходзяцца, і дадзіце функцію пераранжавання, калі патрэбна лепшая якасць адпаведзей. Кожная дапамога такім методам коштуе ў вазе сложнасці, затрымкі і вычысленняў. Для практычнага выкарыстоўвання RAG стандартам являецца гібрыдны пошук плюс пераранжаванне, а сложнасць должна з’яўляцца толькі тады, калі для гэтага ёсць конкрэтная прычына.
Адна практычная прыметка пра пераранжаванне: для яго ўстановкі трэба выконаць pip install sentence-transformers, а модэль BAAI/bge-reranker-base автаматычна завантажваецца праз першыя выкарыстоўвання, яго размах адпаведае прыблізна 1 ГБ. Для практычнага выкарыстоўвання API-сервісы пераранжавання ад Cohere чыста Jina AI дапамагаюць ухіліцца ад непатрэбных зусіль, якія трэба віддаць на запуск модэля локальна.
Супаўзяленыя матэрыялы
- За межамі косайнай сэмліярнасці: пяць тэхнік адзыскання дакументаў у RAG, якія былі парабялены — Косайн, BM25, гібрыдны RRF, пераранкінг за дапамою крос-энкодэраў і MMR, адказаныя з прыкладамі коду, перавагамі і спосабам, як спачатку выбраць тэхніку адзыскання, якая патрэбна для вашага RAG.
- Пошук і адгук для RAG: чаму адзысканне дакументаў патрабуе аддзельнага этапу пераранкінгу — Сэмліярнасць умяшчэнняў выявляе кандыдатаў, але не можа адгукнуцца пра ўзаемнае адносленне. Пазнакоміцеся з двухэтапным патэранам пошуку і адгуку з вікорыстаннем RRF, крос-энкодэраў, ColBERT і алгорытмаў пераранкінгу LLM.