RAG Retriever 실무

Reranker와 Metadata Filter

2026. 06. 29.
AIRAGRetrieverReranker

앞 글에서 Hybrid Retriever로 top-20 후보를 만들었다. 근데 여기서 그대로 LLM에 다 넣으면 두 가지 문제가 생긴다.

1. 컨텍스트 윈도우 / 비용이 폭발한다
   (top-20을 다 넣으면 토큰이 너무 많아짐)

2. 진짜 중요한 문서가 뒤에 묻힌다
   (Retriever 점수만으로는 정확도가 부족함)

이 두 문제를 풀어주는 게 Reranker다. 그리고 권한, 날짜, 부서 같은 운영 요구는 Metadata Filter로 처리한다.

이번 글에서는 둘을 다룬다.


Reranker란 뭔가

Retriever는 빠르게 후보를 추리는 단계다. Reranker는 그 후보를 더 비싼 모델로 다시 정렬하는 단계다.

글로 풀면 이렇다.

원본 100만 문서
  ↓ Retriever (Hybrid)   ← 빠름. top-50~1000 추림
50~1000개 후보
  ↓ Reranker             ← 느림/비쌈. top-5~10 정밀 정렬
최종 top-5
  ↓ LLM

왜 두 단계로 나누나? 한 단계로 다 하면 너무 비싸기 때문이다.

Retriever  : 문서 100만 개 다 계산해도 빨라야 함
            → 가벼운 모델로 점수 매김

Reranker   : 후보 50개만 계산하면 됨
            → 무거운 모델로 정밀하게 점수 매김

Reranker는 어떤 모델인가

대표적인 옵션 3종.

1. Cross Encoder (오픈소스)

입력: (질문, 문서) 한 쌍
출력: 0~1 사이의 관련도 점수

질문과 문서를 같이 모델에 넣어서 점수를 매긴다. 임베딩 모델(질문 따로, 문서 따로 벡터화)보다 정확하지만 그만큼 느리다.

from sentence_transformers import CrossEncoder
 
model = CrossEncoder("BAAI/bge-reranker-v2-m3")
pairs = [(query, doc) for doc in candidates]
scores = model.predict(pairs)

대표 모델:

- BAAI/bge-reranker-v2-m3    (한국어 포함 다국어)
- BAAI/bge-reranker-large
- jinaai/jina-reranker-v2-base-multilingual

2. Cohere Rerank (API)

import cohere
co = cohere.Client(api_key="...")
 
result = co.rerank(
    query="연차 얼마나 있어?",
    documents=candidate_texts,
    model="rerank-multilingual-v3.0",
    top_n=5,
)

API라 운영 부담이 적고, 다국어 품질이 좋아 한국어 RAG에서 인기. 단점은 비용과 외부 의존성.

3. LLM as Reranker

GPT-4o나 Claude에게 후보를 보여주고 "관련도 순으로 정렬해줘"라고 시키는 방식.

- 가장 비쌈
- 가장 유연함 (의도, 맥락까지 고려)
- top-N이 작을 때만 (5~20개)

품질은 가장 높지만 latency와 비용이 큰 문제라, 마지막 단계의 한 번만 쓰는 게 보통이다.


어느 걸 골라야 하나

대충 이렇게 시작하면 된다.

1. 처음에는 Reranker 없이 시작 (Hybrid만)
   → 평가 지표(MRR, nDCG) 측정

2. 품질이 부족하면 bge-reranker-v2-m3 부터 시도
   (자체 호스팅, 비용 없음)

3. 한국어 품질이 부족하거나 운영이 부담이면 Cohere Rerank
   (API, 월 비용 발생)

4. 마지막 한 끗을 위해 LLM Rerank 검토
   (top-5 안에서 순서만 바꾸는 용도)

LTR (Learning To Rank)는 왜 잘 안 가는가

검색 책을 보면 "LTR로 검색 품질을 끌어올린다"는 말이 자주 나온다. 사용자 클릭/피드백 로그를 학습 데이터로 써서 랭킹을 개선하는 기법이다.

사용자가 1번 결과를 클릭함 → 1번이 정답
사용자가 1번을 보고 2번을 클릭함 → 2번이 더 정답

이걸 모아서 모델을 학습시키면 랭킹이 좋아진다.

그런데 대부분의 회사/프로젝트에서 RAG에 LTR까지 가는 경우는 드물다. 이유는 단순하다.

1. 충분한 트래픽이 필요함 (수만~수십만 쿼리/일)
2. 사용자 피드백을 받을 UI가 필요함
3. 정답 라벨링이 필요함
4. 평가 체계가 잡혀 있어야 ROI 측정 가능
5. Cohere Rerank로도 대부분 충분히 좋음

그래서 실무 RAG의 80%는 Hybrid + Reranker(Cohere or bge) 에서 멈춘다. LTR은 검색 서비스(쇼핑, 뉴스, 채용) 회사에서 의미가 크지, 사내 RAG에서는 보통 ROI가 안 나온다.


Metadata Filter: 운영의 절반이 여기서 일어난다

Retriever만 잘 만들면 끝일 것 같지만, 실무에서는 검색 결과를 잘라내는 일이 절반이다. 대표적인 시나리오.

1. 사용자별 권한
   → "이 사용자는 인사팀이 아니므로 인사 문서는 제외"

2. 날짜
   → "최근 6개월 문서만"
   → "은퇴한 정책 문서는 제외"

3. 문서 타입
   → "FAQ만 검색"
   → "이번 시즌 매뉴얼만"

4. 프로젝트 / 부서 / 채널
   → "내가 참여 중인 프로젝트 문서만"

이걸 못 막으면 유출 사고가 난다. LLM은 "이건 인사 문서니까 답하지 마"라고 시켜도 무시할 수 있다. 가져오기 전에 자르는 게 안전하다.


Filter는 어디서 거는가

세 가지 위치가 가능하다.

1. Retriever 안에서 (Pre-filter)
2. 후보가 나온 뒤 (Post-filter)
3. LLM 프롬프트 안에서 (Soft-filter)

추천은 단호하다.

권한, 보안 관련 필터 → 무조건 1번 (Pre-filter)
운영 편의 필터       → 1번 또는 2번
프롬프트로 거르기    → 거의 안 됨, 절대 권한 필터로 쓰지 말 것

Elasticsearch / OpenSearch는 BM25 쿼리에 필터를 같이 넣으면 된다.

{
  "query": {
    "bool": {
      "must": [
        { "match": { "content": "휴가" } }
      ],
      "filter": [
        { "terms": { "allowed_user_ids": ["u123"] } },
        { "range": { "updated_at": { "gte": "2026-01-01" } } },
        { "term": { "doc_type": "policy" } }
      ]
    }
  }
}

pgvector도 SQL의 WHERE 절을 그대로 쓸 수 있다.

SELECT id, content, embedding <=> $query_vec AS distance
FROM documents
WHERE 'u123' = ANY(allowed_user_ids)
  AND updated_at >= '2026-01-01'
  AND doc_type = 'policy'
ORDER BY distance
LIMIT 50;

Pinecone, Qdrant 같은 전용 벡터 DB도 metadata filter를 지원한다.


권한 필터 설계 팁

권한 필터는 한 번 잘못 설계하면 평생 따라다닌다. 짧게 핵심만.

1. 문서마다 "허용된 주체 목록"을 박아둔다

allowed_user_ids: [u123, u456]
allowed_group_ids: [g_eng, g_sec]
visibility: "public" | "internal" | "private"

2. 검색 시점에 사용자 컨텍스트를 동적으로 만든다

사용자가 속한 그룹: [g_eng, g_payments]
사용자 ID: u123

→ 필터: allowed_user_ids contains u123
       OR allowed_group_ids overlaps [g_eng, g_payments]
       OR visibility = "public"

3. 삭제가 아니라 숨김으로 관리

문서가 비공개로 바뀌면 인덱스에서 지우지 말고 visibility만 바꾸자. 나중에 권한이 다시 풀릴 수 있고, 삭제는 인덱스 재구축 비용이 크다.

4. 권한 캐시는 짧게

사용자 그룹 캐시는 1~5분 정도만
어제 부서가 바뀐 사람이 어제 문서를 못 봐도 곤란하니까

Reranker와 Filter는 어디 들어가나

전체 파이프라인에서 위치는 이렇다.

질문
  ↓
[Sparse: BM25 + Metadata Filter (권한, 날짜, 타입)]
  ↓ top-50
[Dense: Vector Search + Metadata Filter]
  ↓ top-50
[RRF 합산]
  ↓ top-20
[Reranker]
  ↓ top-5
[LLM에 컨텍스트로 주입]

핵심 원칙은 이거다.

- Filter는 가장 앞에 (안전성, 검색 효율)
- Reranker는 가장 뒤에 (비싼 정밀 정렬은 후보가 적어진 뒤)
- 두 단계 사이에서 점수를 그대로 노출해야 디버깅 가능

정리

  • Reranker는 후보를 더 비싼 모델로 다시 정렬하는 단계다
  • 옵션: bge-reranker(자체 호스팅), Cohere Rerank(API), LLM Rerank(가장 비쌈)
  • LTR까지 가는 건 트래픽/피드백/평가 체계가 갖춰진 회사에서나 의미 있다
  • Metadata Filter는 권한/날짜/타입/부서 등 운영의 핵심이다
  • 권한 필터는 무조건 Pre-filter, 프롬프트로 거르지 말 것

다음 글에서는 "왜 RAG 검색 품질이 60~70%에서 멈추는가" 를 9가지 원인으로 정리하고, 실무 검색 프로세스 13단계와 함께 RAG Evaluation을 어떻게 운영하는지 다룬다.