k
korAI
고급 전체
🔥 고급2026-04-1610분

RAG 품질을 2배 끌어올리는 하이브리드 검색

벡터 유사도만으로는 한계가 있다. BM25 + 벡터 + 재순위화 3단 구조로 리콜과 정밀도 동시 상승.

ragsearch

벡터 검색의 한계

  • 희소 키워드에 약하다. "에러코드 E4521" 같은 정확 매칭 못 잡음
  • 의미상 비슷한데 주제 다른 문서가 섞임
  • 한국어 임베딩 품질이 영어만큼 안 좋을 때 있음

하이브리드 파이프라인

쿼리
  │
  ├─→ BM25 검색 (Top 50)  ─┐
  │                         ├─→ 합집합 후보 100개
  └─→ 벡터 검색 (Top 50)  ─┘
                                 │
                                 ↓
                          재순위화 (Reranker)
                                 │
                                 ↓
                             Top 5 → LLM

스코어 합성 (RRF)

Reciprocal Rank Fusion이 실무에서 가장 안정적.

function rrf(bmRanks: Map<string, number>, vecRanks: Map<string, number>, k = 60) {
  const scores = new Map<string, number>()
  const ids = new Set([...bmRanks.keys(), ...vecRanks.keys()])
  for (const id of ids) {
    const bm = bmRanks.get(id) ?? 1000
    const vc = vecRanks.get(id) ?? 1000
    scores.set(id, 1/(k + bm) + 1/(k + vc))
  }
  return [...scores.entries()].sort((a, b) => b[1] - a[1])
}

재순위화 (Reranker)

Top 100을 LLM에 한 번에 안 넣는다. Cross-encoder reranker 또는 Haiku를 판사로 쓴다.

async function rerank(query: string, candidates: Doc[]): Promise<Doc[]> {
  const msg = await client.messages.create({
    model: "claude-haiku-4-5",
    max_tokens: 500,
    tools: [{
      name: "rank",
      description: "문서 ID를 관련도 순으로 정렬",
      input_schema: {
        type: "object",
        properties: {
          ordered_ids: { type: "array", items: { type: "string" } },
        },
        required: ["ordered_ids"],
      },
    }],
    tool_choice: { type: "tool", name: "rank" },
    messages: [{
      role: "user",
      content: `쿼리: ${query}\n\n후보:\n${candidates.map(c => `[${c.id}] ${c.text.slice(0, 300)}`).join("\n")}\n\n관련도 순으로 정렬하라.`,
    }],
  })
  const call = msg.content.find(c => c.type === "tool_use") as any
  const order: string[] = call.input.ordered_ids
  return order.map(id => candidates.find(c => c.id === id)!).filter(Boolean)
}

청킹 팁

  • 500~1000 토큰이 일반적 스위트스팟
  • 문서 구조 기반 분할 (### 헤더 기준) > 고정 길이
  • 오버랩 10~20% 경계 손실 방지
  • 각 청크에 parent_id + 제목을 메타데이터로 붙여 나중에 컨텍스트 재구성

평가 지표

  • Recall@k: Top-k 안에 정답 문서가 있는 비율
  • MRR: Mean Reciprocal Rank, 정답이 몇 번째에 있나
  • LLM 답변 정확도: 검색이 좋아도 최종 답이 틀리면 의미 없음

체크리스트

  • [ ] BM25 + 벡터 병렬
  • [ ] RRF로 합성
  • [ ] Reranker로 Top 5~10 추림
  • [ ] 청크에 parent_id 메타
  • [ ] Recall/MRR 주기 측정