본문 바로가기
C.W.K.
Stream
Lesson 04 of 05 · published

필요할 때만 더 깊게 다시 매기기

~12 min · rerank, cross-encoder, deepening, retrieve-then-rerank

Level 0꺼진 심지
0 XP0/33 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"싼 검색은 그물을 넓게 던지고, reranker는 이미 잡힌 후보만 가까이 보는 비싼 눈이야."

관련성을 보는 두 가지 모델

벡터 검색의 bi-encoder는 쿼리와 문서를 따로 임베딩한 뒤 두 벡터를 비교해. 문서 벡터를 미리 계산할 수 있어서 수백만 건으로 넓혀도 빠르지만, 모델 안에서 쿼리와 문서가 직접 만나지는 않아. cross-encoder reranker는 둘을 한꺼번에 넣고 한 쌍의 관련성을 계산해. 표현 사이의 세밀한 관계를 더 잘 보지만 미리 계산할 수 없어.

정확하게 만드는 성질이 비용도 키워

cross-encoder는 모든 쿼리와 문서 조합마다 새 추론을 해야 해. 후보가 천 개면 천 쌍을 지금 계산해야 하고, 코퍼스 전체에 적용하면 비용이 감당되지 않아. 쿼리와 문서를 함께 읽는 덕분에 정확하지만 바로 그 이유로 느린 거야.

먼저 넓게 찾고, 그다음 좁게 깊이 봐

Lantern은 BM25와 벡터, RRF로 평소보다 넓은 후보를 싸게 모아. 최종 8개가 필요하다면 먼저 50개를 가져오는 식이야. 첫 단계는 정답이 후보 집합 어딘가에 들어오도록 재현율을 높이고, cross-encoder는 그 50개만 다시 점수 매겨 가장 정밀한 8개를 고르지. 백만 문서를 비싸게 읽지 않고도 rerank의 장점을 얻는 방법이야.

비싼 정밀도는 값싼 고재현율 단계 뒤에 둬. 먼저 놓치지 않는 필터로 범위를 줄이고, 깊은 검사는 남은 후보에게만 써. 이 순서는 검색뿐 아니라 느린 테스트와 수동 검토에도 그대로 적용돼.

필요할 때만 켜고, 못 돌렸다면 밝혀

rerank는 모델 호출 비용이 있으니 기본값으로는 끄고 쿼리마다 선택해. reranker에 닿지 못하면 앞 단계의 결과를 돌려줄 수 있지만, reranker_unavailable이라고 기능 저하를 명시해야 해. rerank하지 않은 순서를 rerank한 것처럼 꾸미면 안 돼. 심화 검색은 사용자가 의식적으로 고르는 추가 정밀도지, 숨어 있다 조용히 사라지는 단계가 아니야.

후보 50이라는 숫자는 장식이 아니라 비용과 품질 사이의 절충을 드러내. 너무 작으면 첫 단계가 놓친 정답을 reranker가 구할 수 없고, 너무 크면 비싼 쌍별 추론이 늘어나. 정답이 거의 항상 포함될 만큼 넓되 비용을 감당할 수 있는 범위를 측정해 골라야 해. 최종 8개보다 넓은 이유가 바로 정답을 먼저 넓게 확보하기 위해서야.

Code

후보 50개를 싸게 찾고 다시 매겨 상위 8개만 돌려줘·python
def search_with_optional_rerank(query: str, k: int = 8, rerank: bool = False):
    if not rerank:
        # Cheap path: hybrid retrieval returns the top-k directly.
        return hybrid(query, k=k)

    # Deepening path: retrieve WIDE cheaply, then rerank NARROW expensively.
    pool = hybrid(query, k=50)                      # high recall, rough order, cheap
    if not reranker.reachable():
        # Honest degradation — do NOT pretend the pool was reranked.
        return Envelope(results=pool[:k], degraded=["reranker_unavailable"])

    scored = reranker.score(query, [c.text for c in pool])  # 50 model calls, not 1e6
    reranked = [c for c, _ in sorted(zip(pool, scored), key=lambda p: -p[1])]
    return Envelope(results=reranked[:k], degraded=[])

External links

Exercise

네 작업에서 돌리는 비싼 검사를 떠올려 — 느린 테스트, 수동 리뷰, 비싼 API 호출. 전부에 돌릴래, 아니면 싸게 걸러낸 후보부터 돌릴래? 2단계 버전을 설계해: 비싼 단계 전에 후보 집합을 줄일 싸면서 재현율 높은 필터가 뭘까? 비싼 연산을 얼마나 아낄지 추정해.
Hint
레시피는 항상 같아: 진짜 답을 거의 안 버리는 싼 필터(높은 재현율)가 후보 범위를 좁히고, 비싸고 정밀한 단계는 남은 것에만 적용해. 비싼 단계를 전체 집합에 돌리는 중이면, 싼 필터가 이미 거절했을 후보에 정밀도 비용을 내는 거야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.