0Pricing
AI Engineering Academy · 강의

2단계 검색이 작동하는 이유

단일 단계 검색에서 재현율과 정밀도가 어떻게 상충하는지 이해하고, 빠른 대략적 검색기 뒤에 느리지만 정확한 재순위 지정기를 배치하면 두 방식의 장점을 모두 얻는 이유를 알아봅니다.

2단계 검색이 작동하는 이유은(는) CoddyKit의 무료 AI Engineering Academy 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 AI Engineering Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. AI Engineering Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

The Recall-Precision Trade-off in Retrieval

Every retrieval system faces a fundamental trade-off: recall measures how many relevant documents you find (did you miss any?), while precision measures how accurate the top results are (how many retrieved docs are actually relevant?). Maximizing both simultaneously is computationally expensive. Fast retrievers sacrifice precision for recall; precise rankers sacrifice speed for accuracy.

Bi-Encoder vs Cross-Encoder: The Core Distinction

The two types of models at the heart of two-stage retrieval differ in how they see the query and document. A bi-encoder encodes the query and each document independently and measures similarity between their vectors — fast but limited by independent encoding. A cross-encoder sees the query and document concatenated as a single input, enabling deep interaction between them — highly accurate but O(n) complexity over the candidate set.

# Bi-encoder: compute query embedding ONCE, compare to all doc embeddings
# O(1) query encoding + O(n) dot products via ANN index = fast
query_vec = embed(query)  # done once
results = vector_index.search(query_vec, top_k=100)  # fast ANN search

# Cross-encoder: re-scores (query, doc) pairs jointly
# O(k) forward passes for k candidate documents = slow but accurate
for doc in results[:100]:
    score = cross_encoder.score(query, doc.text)  # joint scoring

Stage 1: Fast Coarse Retrieval

The first stage is a fast retriever — typically a bi-encoder with an approximate nearest neighbor index or a BM25 index — that retrieves a large candidate set (50-200 documents) with high recall but modest precision. The goal is not to be accurate; it is to not miss relevant documents. We cast a wide net and accept some false positives, knowing the second stage will clean them up.

from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings

# Stage 1: retrieve 100 candidates (high recall, modest precision)
vectorstore = FAISS.from_documents(documents, OpenAIEmbeddings())
coarse_retriever = vectorstore.as_retriever(
    search_kwargs={'k': 100}  # large candidate set
)

candidates = coarse_retriever.invoke(query)
print(f'Stage 1: retrieved {len(candidates)} candidate documents')

Stage 2: Accurate Cross-Encoder Re-ranking

The second stage takes the candidate set from stage 1 and re-scores each (query, document) pair using a cross-encoder that reads both together. Because it processes only 50-200 candidates (not the full corpus), it can afford the expensive joint encoding. The cross-encoder's deep attention over the concatenated input makes it far more accurate at estimating true relevance than a bi-encoder.

from sentence_transformers import CrossEncoder

reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')

def rerank(query: str, candidates: list[str], top_k: int = 5) -> list[str]:
    # Score each (query, document) pair jointly
    pairs = [[query, doc] for doc in candidates]
    scores = reranker.predict(pairs)

    # Sort by score descending
    ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
    return [doc for doc, _ in ranked[:top_k]]

candidate_texts = [doc.page_content for doc in candidates]
final_docs = rerank(query, candidate_texts, top_k=5)
print(f'Stage 2: selected top {len(final_docs)} documents after re-ranking')

Why This Combination Works

The two-stage design exploits a key asymmetry: the first stage's fast ANN search scales to millions of documents in milliseconds, while the second stage's accurate cross-encoder operates only on the small candidate pool. You get the scalability of approximate search with the accuracy of exact joint scoring. The overall pipeline is both fast and highly accurate — something neither stage achieves alone.

Latency Profile of Two-Stage Retrieval

In a typical two-stage pipeline: stage 1 (vector ANN search over 1M documents) takes 5-20ms; stage 2 (cross-encoder over 100 candidates) takes 100-500ms depending on document length and hardware. The total latency budget is 150-600ms — acceptable for most applications. GPU acceleration in stage 2 can reduce re-ranking to under 30ms for short documents, making the pipeline competitive with single-stage retrieval on latency-sensitive applications.

import time

def two_stage_search(query, coarse_retriever, reranker, top_k=5):
    t0 = time.perf_counter()

    candidates = coarse_retriever.invoke(query)        # stage 1
    t1 = time.perf_counter()

    candidate_texts = [c.page_content for c in candidates]
    final_docs = rerank(query, candidate_texts, top_k)  # stage 2
    t2 = time.perf_counter()

    print(f'Stage 1 (retrieval): {(t1-t0)*1000:.1f}ms')
    print(f'Stage 2 (re-ranking): {(t2-t1)*1000:.1f}ms')
    print(f'Total: {(t2-t0)*1000:.1f}ms')
    return final_docs

Choosing the Right Candidate Set Size

The first stage candidate set size is a critical hyperparameter. Too small (say, 10) and relevant documents may be missed before re-ranking even starts. Too large (say, 500) and stage 2 latency blows up. The recall at N curve — how many relevant documents are captured at different values of N — guides this choice. Typical sweet spots are between 50 and 150 candidates, where recall is near-saturation but latency remains manageable.

def recall_at_n(coarse_retriever, test_queries, golden_relevant, n_values):
    for n in n_values:
        recalls = []
        for query, relevant in zip(test_queries, golden_relevant):
            # Temporarily set k to n
            coarse_retriever.search_kwargs['k'] = n
            results = coarse_retriever.invoke(query)
            retrieved_ids = {r.metadata.get('id') for r in results}
            relevant_found = len(set(relevant) & retrieved_ids)
            recalls.append(relevant_found / len(relevant))
        avg = sum(recalls) / len(recalls)
        print(f'N={n}: recall={avg:.3f}')

Hybrid First Stage + Cross-Encoder Second Stage

The most powerful two-stage configuration pairs a hybrid retriever (dense + BM25) as the first stage with a cross-encoder as the second stage. Hybrid retrieval maximizes first-stage recall by combining semantic and keyword matching, and the cross-encoder then accurately selects the most relevant documents from the combined candidate pool. This configuration consistently achieves state-of-the-art retrieval quality on benchmarks.

from langchain.retrievers import EnsembleRetriever

# Stage 1: hybrid retrieval for maximum recall
hybrid_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.4, 0.6],
)

# Stage 2: cross-encoder re-ranking for high precision
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder

cross_encoder_model = HuggingFaceCrossEncoder(model_name='cross-encoder/ms-marco-MiniLM-L-6-v2')
compressor = CrossEncoderReranker(model=cross_encoder_model, top_n=5)

from langchain.retrievers import ContextualCompressionRetriever
two_stage = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=hybrid_retriever,
)

Commercial Re-ranking APIs

If you want cross-encoder accuracy without managing your own model, both Cohere Rerank and Jina AI Reranker offer cloud-hosted re-ranking APIs. You send a query and a list of document texts, and receive relevance scores back. These APIs use large cross-encoder models (often 500M+ parameters) that outperform self-hosted small cross-encoders, at the cost of additional API latency (50-300ms) and pricing per reranked document.

import cohere

co = cohere.Client('YOUR_API_KEY')

def cohere_rerank(query: str, documents: list[str], top_k: int = 5):
    response = co.rerank(
        model='rerank-english-v3.0',
        query=query,
        documents=documents,
        top_n=top_k,
    )
    return [
        {'text': documents[r.index], 'score': r.relevance_score}
        for r in response.results
    ]

final = cohere_rerank(query, candidate_texts, top_k=5)
for doc in final:
    print(f'Score {doc["score"]:.3f}: {doc["text"][:80]}')

When Two-Stage Is Overkill

Two-stage retrieval adds complexity and latency compared to single-stage. It is not always necessary. For small corpora under 10,000 documents, a single cross-encoder over the full set may be fast enough. For applications where latency under 100ms is critical and accuracy gains are modest, single-stage dense retrieval may be preferable. Use two-stage when you have a large corpus, high accuracy requirements, and can afford 200-500ms retrieval latency.

Three-Stage Retrieval for Extreme Scale

For corpora of tens of millions of documents, a three-stage pipeline is sometimes used: first stage retrieves 10,000 candidates with ANN, second stage re-ranks to 100 using a fast small cross-encoder, and third stage re-ranks to 5 using a large powerful cross-encoder. Each stage applies a more expensive and accurate model to a smaller set. This architecture is used by large-scale search engines and document Q&A systems.

Quick Check

Test your understanding of why two-stage retrieval works from this lesson.

Lesson Recap

In this lesson you learned: bi-encoders are fast but limited to independent query-document encoding, cross-encoders are accurate through joint encoding but too slow for full-corpus search, and two-stage retrieval combines both: a fast first stage for high recall followed by an accurate second stage for high precision. The first stage retrieves many more candidates than needed to avoid missing relevant documents. Next up we implement cross-encoder re-ranking with Cohere and BGE.

자주 묻는 질문

“2단계 검색이 작동하는 이유” 강의는 무료인가요?

네 — “2단계 검색이 작동하는 이유” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 AI Engineering Academy 강의 전체를 잠금 해제할 수 있습니다. AI Engineering Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“2단계 검색이 작동하는 이유”에서 뭘 배우나요?

단일 단계 검색에서 재현율과 정밀도가 어떻게 상충하는지 이해하고, 빠른 대략적 검색기 뒤에 느리지만 정확한 재순위 지정기를 배치하면 두 방식의 장점을 모두 얻는 이유를 알아봅니다. 브라우저에서 직접 실행하는 실습 코드로 AI Engineering Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

AI Engineering Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 AI Engineering Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.

“2단계 검색이 작동하는 이유” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 AI Engineering Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 AI Engineering Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 2단계 검색이 작동하는 이유
  2. Cohere와 BGE를 활용한 교차 인코더 재순위 지정
  3. 문맥 압축과 관련성 필터링
  4. 재순위 지정의 영향 측정
← AI Engineering Academy(으)로 돌아가기