C.W.K.
Stream
Lesson 04 of 05 · published

심화 패스로서의 Rerank

~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) 는 쿼리 문서를 함께 모델에 넣고 쌍을 함께 점수 매겨. 모델이 둘을 한 번에 attend 해서, 분리된 비교가 놓치는 미묘한 관련성을 잡아.

더 낫고 더 느린 건 같은 속성이야

cross-encoder 가 더 정확한 건 바로 쿼리와 문서를 함께 보기 때문이고 — 그게 정확히 비싼 이유야. 아무것도 미리 계산 못 해: 모든 쿼리-문서 쌍이 새 모델 호출이야. 천 개 후보를 rerank 하면 천 번의 forward pass 를, 지금, 캐싱 불가능하게. 정확하게 만드는 바로 그것(joint attention) 이 코퍼스 전체에 돌리기엔 너무 비싸게 만드는 거야. 그래서 안 해.

심화 패턴: 넓게 검색, 좁게 rerank

해결은 2단계 파이프라인이야. 먼저 싼 방법 — BM25 와 벡터, 융합된 — 으로 평소보다 넓은 후보 풀을 당겨: 상위 8 대신 상위 50 을 가져와. 이 싼 검색기는 순서가 거칠어도 recall(옳은 답을 풀 어딘가에 넣기) 에 뛰어나. 그다음 비싼 cross-encoder 를 그 50 에만 돌리고, 그 상위 8 을 잡아. 백만이 아니라 50 번의 모델 호출을 썼고, 정밀도로 순위된 결과를 얻어. 넓고 싸게 검색하고, 좁고 비싸게 rerank 해.

비싼 정밀도를 싼 recall 뒤에 둬, 대신이 아니라. 패턴은 검색을 훨씬 넘어 일반화돼: 싸고 high-recall 필터로 후보 집합을 줄이고, 비싸고 high-precision 방법을 살아남은 것에만 써. 비싼 단계를 전부에 돌리는 건 낭비고, 잘 고른 shortlist 에 돌리는 게 그 정확성이 실제로 값을 하는 곳이야. 두 단계가 하나의 영웅적 단계를 이겨.

Opt-in, 그리고 못 돌릴 땐 정직

rerank 는 진짜 모델 호출이 드니까, Lantern 은 기본으로 꺼둬 — 추가 정밀도가 값할 때 켜는 쿼리별 플래그. 그리고 모델 서버를 부르니, 벡터 검색과 같은 정직 규칙을 따라: reranker 가 안 닿으면, 요청이 degrade 하고 그렇다고 말해, rerank 안 된 순서를 rerank 된 것처럼 꾸며서 조용히 돌려주는 대신. 심화는 눈 뜨고 opt-in 하는 능력이지, 조용히 실패하는 숨은 단계가 아냐.

Code

싸게 50 검색, 그 50 을 rerank, 상위 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 호출. 전부에 돌려, 아니면 싸게 걸러진 shortlist 에 먼저? 2단계 버전을 설계해: 비싼 단계 전에 후보 집합을 줄일 싸고 high-recall 필터가 뭘까? 비싼 연산을 얼마나 아낄지 추정해.
Hint
레시피는 항상 같아: 진짜 답을 거의 안 버리는 싼 필터(high recall) 가 밭을 좁히고, 비싸고 정밀한 단계가 살아남은 것에만 돌아. 비싼 단계를 전체 집합에 돌리는 중이면, 싼 필터가 이미 거절했을 후보에 정밀도 비용을 내는 거야.

Progress

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

댓글 0

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

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