사내 AI 활용 가이드

정확하게 찾아내는 방법
벡터 DB · 하이브리드 검색 · 리랭킹

벡터DB 하이브리드검색 리랭킹
연구소 기술노트 · 2026년 8월 · 읽는 데 약 16분
3/5 시리즈 사내 문서 AI · 기초 — 심화

이 글의 목표 — ① 수백만 건 중에서 밀리초 안에 찾아내는 원리 ② 의미 검색이 놓치는 것과 그 보완책 ③ 검색 결과를 다시 줄 세우는 리랭킹 ④ 질문 종류에 따라 다른 길로 보내는 방법을 이해합니다.

2편에서 문서를 조각내고 벡터로 바꿔 저장하는 데까지 왔습니다. 이제 질문이 들어왔을 때 그 많은 조각 중에서 정확히 필요한 것만 골라내는 방법을 다룹니다.

RAG에서 잘못된 답이 나오는 원인의 대부분은 이 단계에 있습니다. 엉뚱한 조각이 LLM에게 전달되면, LLM은 그 엉뚱한 조각을 근거로 성실하게 잘못된 답을 씁니다.

1. 벡터 데이터베이스가 따로 필요한 이유

회사에서 쓰는 일반적인 데이터베이스는 정확한 값을 찾는 데 최적화되어 있습니다. "사번이 12345인 사람"이나 "3월 매출 합계" 같은 질의는 순식간에 처리합니다.

그런데 우리가 하려는 일은 다릅니다. "이 벡터와 방향이 가장 비슷한 벡터 열 개를 찾아라"입니다. 일반 데이터베이스에는 이런 기능이 없어서 저장된 모든 벡터와 하나씩 비교해야 합니다. 수백만 건이면 수 초에서 수십 초가 걸립니다. 챗봇으로는 쓸 수 없는 속도입니다.

그래서 이 작업만을 위해 만들어진 벡터 데이터베이스를 씁니다.

이름특징
Milvus대규모 자체 서버 운영에 강하고, 뒤에 설명할 하이브리드 검색을 기본 지원합니다
Pinecone클라우드 서비스형이라 운영 부담이 적습니다
Chroma가볍고 설치가 간단해 처음 배울 때 적합합니다
FAISS검색 속도가 매우 빠른 라이브러리입니다
Qdrant / Weaviate오픈소스이며 각각 필터링·멀티모달에 강점이 있습니다
pgvectorPostgreSQL 확장이라 기존 데이터베이스를 그대로 쓸 수 있습니다

사내망에서 대량의 문서를 다룬다면 Milvus가 무난합니다. 아래 설명은 Milvus를 기준으로 하지만, 원리는 어느 제품이든 같습니다.

저장되는 내용

벡터만 저장하면 검색은 되지만 답을 만들 수 없습니다. 2편에서 말씀드린 대로 원문과 출처를 함께 저장해야 합니다.

{
  "id": 1042,
  "vector": [0.12, -0.09, ...],        ← 검색에 사용
  "text":   "이용 수수료는 서비스 유형에 따라...",   ← LLM에게 전달
  "source": "이용안내.pdf",             ← 출처 표기에 사용
  "page":   4,
  "doc_type": "manual"                  ← 문서 종류로 걸러낼 때 사용
}

2. 밀리초 검색의 비밀 — HNSW

벡터 데이터베이스가 빠른 이유는 전부 비교하지 않기 때문입니다.

가장 널리 쓰이는 방법이 HNSW입니다. 벡터들을 미리 그물처럼 연결해 두고, 위층에서 대략적인 방향을 잡은 다음 아래층으로 내려가며 정밀하게 좁혀 갑니다.

지도에서 길을 찾는 것과 비슷합니다. 서울에서 부산의 어떤 골목을 찾을 때 전국의 모든 골목을 뒤지지 않습니다. 고속도로로 부산까지 간 다음, 시내 도로로 동네를 찾고, 마지막에 골목을 찾습니다. HNSW도 같은 방식으로 계층을 오르내립니다.

방식정확도소요 시간 (1천만 건 기준)
전부 비교100%수십 초
HNSW약 97~99%1~10밀리초
IVF약 90~95%5~50밀리초

정확도를 조금 양보하는 대신 속도를 수천 배 얻는 방식입니다. 이런 접근을 근사 검색이라고 합니다. 100건 중 한두 건을 놓칠 수 있지만, 챗봇에서는 사실상 체감되지 않습니다.

설정값의미일반적인 범위
M각 지점이 연결하는 이웃 수16~32
efConstruction그물을 짤 때의 정밀도200~400
ef검색할 때 살펴보는 범위64~128

세 값 모두 크게 잡을수록 정확해지고 느려집니다. 기본값에서 시작해 필요할 때 조정하시면 됩니다.

3. 의미 검색이 놓치는 것들

지금까지 설명한 방식, 즉 임베딩 벡터로 찾는 검색을 의미 검색 또는 Dense 검색이라고 부릅니다. 숫자가 빽빽하게 채워진 벡터를 쓰기 때문에 이런 이름이 붙었습니다.

의미 검색은 표현이 달라도 뜻이 같으면 찾아냅니다. 이것이 가장 큰 장점입니다.

사용자 질문문서에 적힌 표현의미 검색
비용이 얼마인가요수수료 안내찾아냄
데이터 받으려면마이크로데이터 다운로드 절차찾아냄
분석 환경 빌려주나요원격접속 서비스 안내찾아냄

단어가 하나도 겹치지 않는데도 찾아냅니다. 키워드 검색으로는 불가능한 일입니다.

그런데 반대 방향에서는 의외로 약합니다.

사용자 질문왜 놓치는가
R 4.2.1 설치되어 있나요버전 번호는 의미를 담고 있지 않은 숫자열입니다
제3조 4항 내용이 뭔가요조항 번호는 이름표에 가까워 의미로 잡히지 않습니다
KOSIS 인용 형식고유 약어는 일반 단어와 구별되지 않습니다

임베딩 모델 입장에서 "4.2.1"과 "4.2.2"는 거의 같은 것으로 보입니다. 사람에게는 완전히 다른 버전이지만, 의미의 세계에서는 구분되지 않습니다.

이것이 의미 검색만으로 시스템을 만들면 안 되는 이유입니다. 정확히 그 글자를 찾는 방법이 따로 필요합니다.

4. 키워드 검색과 BM25

전통적인 검색은 단어가 실제로 들어 있는지를 봅니다. 이것을 키워드 검색 또는 Sparse 검색이라고 합니다.

이 방식에서 문서는 이렇게 표현됩니다. 어휘 전체 크기의 목록을 두고, 실제로 등장한 단어 자리에만 점수를 매깁니다. 나머지는 전부 0입니다. 대부분이 비어 있어서 "성긴(sparse)"이라는 이름이 붙었습니다.

{"수수료": 0.82,
 "이용":   0.44,
 "SDC":    0.71,
 ... 나머지 99,997개 단어는 전부 0}

점수를 매기는 표준 방법이 BM25입니다. 검색 엔진에서 오래도록 쓰여 온 검증된 방식입니다. 두 가지를 봅니다.

"반출심의"라는 단어가 질문에 들어 있고 특정 문서에만 그 단어가 있다면, BM25는 그 문서를 정확하게 1위로 올립니다. 의미를 전혀 이해하지 못하면서도 말입니다.

항목의미 검색 (Dense)키워드 검색 (BM25)
판단 근거학습된 의미단어 등장 통계
강점동의어, 다른 표현정확한 단어, 고유명사, 번호
약점버전·조항 번호를 놓침표현이 다르면 못 찾음
준비 과정임베딩 모델 필요단어 통계만 있으면 됨

표를 보시면 두 방식의 강점과 약점이 정확히 반대라는 것을 알 수 있습니다. 그렇다면 답은 하나입니다.

5. 하이브리드 검색 — 둘을 합치기

두 검색을 동시에 돌리고 결과를 합치는 것을 하이브리드 검색이라고 합니다. 실무 RAG에서는 사실상 기본 구성입니다.

합칠 때 문제가 하나 있습니다. 두 검색의 점수 체계가 서로 다릅니다. 의미 검색은 0~1 사이의 유사도를 내고, BM25는 상한이 없는 점수를 냅니다. 그냥 더하면 한쪽이 일방적으로 결과를 지배합니다.

그래서 점수 대신 순위를 씁니다. 이 방법을 RRF라고 합니다. 각 검색에서의 순위를 역수로 바꿔 더하는 방식입니다.

점수 = 1 / (60 + 순위)  를 각 검색마다 계산해서 더함

문서 A : 의미 검색 2위, 키워드 검색 1위
   → 1/(60+2) + 1/(60+1) = 0.0161 + 0.0164 = 0.0325

문서 B : 의미 검색 1위, 키워드 검색 5위
   → 1/(60+1) + 1/(60+5) = 0.0164 + 0.0154 = 0.0318

→ 양쪽 모두에서 상위인 A가 최종 1위

결과적으로 한쪽에서만 1등인 문서보다, 양쪽에서 고르게 상위인 문서가 우선됩니다. 사람의 직관과도 잘 맞습니다.

Milvus에서는 이 과정이 기능으로 제공되어 설정만 하면 됩니다.

from pymilvus import AnnSearchRequest, RRFRanker

의미검색 = AnnSearchRequest(data=[벡터], anns_field="dense_vector", limit=10)
키워드검색 = AnnSearchRequest(data=[단어점수], anns_field="sparse_vector", limit=10)

결과 = collection.hybrid_search(
    [의미검색, 키워드검색],
    rerank=RRFRanker(k=60),
    limit=10)

6. 리랭킹 — 다시 줄 세우기

여기까지 해도 문제가 하나 남습니다. 검색은 빠르지만 순위가 항상 정확하지는 않습니다. 정말 필요한 문서가 3위나 4위에 있는 경우가 흔합니다.

예를 들어 "이용 수수료 기준이 어떻게 되나요"라고 물었을 때 이런 결과가 나왔다고 해 보겠습니다.

순위검색 결과점수
1수수료 안내 (2022년)0.87
2이용 요금 정책 개요0.85
3신청 절차 안내0.83
4수수료 상세 기준 (최신)0.81

가장 정확한 문서가 4위에 있습니다. 점수 차이도 0.06밖에 나지 않습니다. 벡터 검색만으로는 "안내"와 "상세 기준"의 차이를 구분하지 못한 것입니다.

왜 이런 일이 생기는가

지금까지의 검색 방식은 질문과 문서를 각각 따로 숫자로 바꾼 뒤 비교합니다. 문서 벡터는 미리 계산해 저장해 둘 수 있어서 대단히 빠릅니다. 대신 질문과 문서를 함께 놓고 따져 보지는 않습니다.

더 정확한 방법이 있습니다. 질문과 문서를 한꺼번에 넣고 "이 둘이 얼마나 맞는가"를 직접 판정하게 하는 것입니다. 훨씬 정확하지만, 미리 계산해 둘 수가 없어서 질문이 바뀔 때마다 모든 문서를 새로 계산해야 합니다.

항목따로 계산 (검색용)함께 계산 (재정렬용)
속도밀리초 — 미리 계산해 둠수백 밀리초 — 그때그때 계산
정확도중간높음
적용 가능 규모수백만 건수십 건

그래서 두 단계로 나눕니다

빠른 방법으로 후보 50개를 먼저 추리고, 그 50개에만 정확한 방법을 적용해 최종 5개를 고릅니다. 이 두 번째 단계를 리랭킹이라고 합니다.

수백만 건에 정밀 계산을 돌리는 것은 불가능하지만 50건이라면 충분히 가능합니다. 앞의 예시에 리랭킹을 적용하면 순서가 이렇게 바뀝니다.

순위리랭킹 전리랭킹 후
1수수료 안내 (2022년)수수료 상세 기준 (최신) — 0.94
2이용 요금 정책 개요수수료 안내 (2022년) — 0.88
3신청 절차 안내이용 요금 정책 개요 — 0.71
4수수료 상세 기준 (최신)신청 절차 안내 — 0.42

점수 분포도 달라졌습니다. 리랭킹 전에는 0.81~0.87 사이에 몰려 있어 구분이 어려웠지만, 리랭킹 후에는 0.42~0.94로 벌어져 관련 없는 문서를 잘라내기 쉬워졌습니다.

이 점수 차이를 활용하면 품질을 한 번 더 올릴 수 있습니다. 예를 들어 0.4 미만은 아예 버리도록 하면, 관련 없는 문서가 LLM에게 전달되는 일을 막을 수 있습니다.

from FlagEmbedding import FlagReranker

reranker = FlagReranker('BAAI/bge-reranker-v2-m3')

질문 = "이용 수수료 기준이 어떻게 되나요?"
후보 = [문서1, 문서2, ...]                    # 하이브리드 검색 결과 50개

쌍 = [[질문, 문서] for 문서 in 후보]           # 질문과 문서를 함께 넣음
점수 = reranker.compute_score(쌍, normalize=True)

상위5개 = [문서 for _, 문서 in
          sorted(zip(점수, 후보), reverse=True)[:5]]

리랭킹은 할루시네이션 감소에 직접 기여합니다. 1편에서 말씀드린 대로 RAG의 잘못된 답은 대부분 검색 실패에서 시작되기 때문입니다.

7. 질문마다 다른 길로 보내기

마지막으로 검색만으로는 풀 수 없는 경우를 다루겠습니다.

"교수가 3일 이용하면 수수료가 얼마인가요"라는 질문을 생각해 보겠습니다. 검색은 수수료 기준표를 잘 찾아옵니다. 그런데 곱셈은 LLM이 합니다. LLM은 계산에 약해서 "대략 15만원 정도"처럼 틀린 답을 낼 수 있습니다.

그래서 질문의 종류를 먼저 판단해 서로 다른 길로 보냅니다.

길어떤 질문인가처리 방법
문서 검색 절차·규정·안내
"신청 방법이 어떻게 되나요"
하이브리드 검색 → 리랭킹 → LLM이 답변 작성
함수 실행 계산·추천
"3일 이용하면 얼마인가요"
계산 함수 실행 → 결과를 LLM이 문장으로 정리
목록 조회 항목 탐색
"소득 변수가 있는 조사는"
목록 데이터 조회 → LLM이 정리해서 제시

어느 길로 보낼지 판단하는 부분을 라우터라고 합니다. 두 단계로 만들면 비용을 아낄 수 있습니다.

# 1차 — 키워드로 빠르게 판단 (LLM 호출 없음, 즉시 처리)
계산_키워드 = ["수수료", "얼마", "비용", "계산", "추천"]
목록_키워드 = ["변수", "항목", "포함된 조사", "데이터 목록"]

# 2차 — 애매하면 LLM에게 물어봄
"""사용자 질문을 아래 셋 중 하나로 분류하세요. JSON으로만 답하세요.
 - "문서검색" : 절차, 규정, 안내
 - "함수실행" : 수수료 계산, 서비스 추천
 - "목록조회" : 조사 목록, 데이터 항목
질문: {질문}"""

LLM이 함수를 호출하게 하기

계산이 필요할 때 쓰는 방식을 Tool Calling이라고 합니다. LLM이 직접 계산하는 대신, 어떤 함수를 어떤 값으로 부를지만 결정하게 하는 것입니다. 실제 계산은 우리가 만든 프로그램이 합니다.

사용자 : "교수인데 인가 서비스 3일 이용하면 얼마예요?"

1단계 — LLM이 호출할 함수와 값을 결정
   {"함수": "수수료계산",
    "값": {"서비스": "인가", "일수": 3, "구분": "교수"}}

2단계 — 우리 프로그램이 실제로 계산
   수수료계산(서비스="인가", 일수=3, 구분="교수")
   → {"기간제": 90000, "정액제": 120000, "추천": "기간제"}

3단계 — 결과를 LLM에게 돌려주고 문장으로 만들게 함
   "교수님이 인가 서비스를 3일 이용하실 경우 기간제는 9만원,
    정액제는 12만원입니다. 3일 이내라면 기간제가 유리합니다."

계산은 프로그램이 하고 LLM은 말만 만듭니다. 그래서 결과가 항상 정확합니다. 실제 함수는 평범한 코드입니다.

요금표 = {
  "공공": {"교수": 20000, "학생": 10000, "일반": 30000},
  "인가": {"교수": 30000, "학생": 15000, "일반": 50000},
}

def 수수료계산(서비스, 일수, 구분):
    하루 = 요금표[서비스][구분]
    return {"기간제": 하루 * 일수,
            "정액제": 하루 * 5,
            "추천": "기간제" if 일수 < 5 else "정액제"}

문서에 답이 없을 때

검색 점수를 보고 대응을 바꾸는 방법도 있습니다.

최고점수 = 검색결과 중 가장 높은 점수

if   최고점수 >= 0.75 :   내부 문서로 충분 → 그대로 답변
elif 최고점수 >= 0.50 :   부분적으로 관련 → 추가 자료와 함께 답변
else                  :   관련 문서 없음 → "찾을 수 없습니다"로 답변

주의 — 기준점을 너무 높이면 문서가 있는데도 못 찾았다고 답하고, 너무 낮추면 관련 없는 문서로 엉뚱한 답을 만듭니다. 0.6~0.75 사이에서 시작해 실제 질문 기록을 보며 조정하시기 바랍니다. 처음부터 정답을 맞히려 하지 말고, 운영하면서 맞춰 가는 값이라고 생각하시는 편이 좋습니다.

8. 자주 나오는 질문

검색이 잘 안 될 때 무엇부터 손봐야 하나요?

순서를 권하자면 이렇습니다. 첫째, 청킹을 확인하세요. 조각이 너무 크거나 엉뚱한 곳에서 잘렸을 가능성이 가장 높습니다. 둘째, 리랭킹을 붙여 보세요. 효과 대비 작업량이 가장 적습니다. 셋째, 고유명사나 번호를 놓치고 있다면 하이브리드 검색을 추가하세요. 임베딩 모델 교체는 마지막에 고려할 일입니다.

리랭킹을 하면 느려지지 않나요?

느려집니다. 보통 수백 밀리초가 추가됩니다. 다만 그 뒤에 이어지는 LLM 답변 생성이 몇 초씩 걸리므로 전체에서 차지하는 비중은 크지 않습니다. 정확도 향상 폭을 생각하면 대부분의 경우 붙이는 편이 낫습니다.

후보를 50개나 뽑아야 하나요?

문서 양과 리랭커 속도에 따라 조정하시면 됩니다. 문서가 적다면 20개로도 충분하고, 많다면 100개까지 늘리기도 합니다. 너무 적게 뽑으면 정답이 애초에 후보에 없어서 리랭킹을 해도 소용이 없습니다. 넉넉히 뽑아 정밀하게 거르는 것이 이 구조의 취지입니다.

하이브리드 검색이 꼭 필요한가요?

다루는 문서에 달렸습니다. 서술형 안내문 위주라면 의미 검색만으로도 잘 동작합니다. 그러나 규정 조항, 제품 코드, 버전 번호, 약어가 자주 등장하는 문서라면 반드시 필요합니다. 사내 문서는 대개 후자에 해당합니다.

내부 문서에 답이 없으면 웹 검색을 붙일 수 있나요?

기술적으로는 가능하며 흔히 쓰이는 구성입니다. 다만 사내망에서는 외부 접속이 막혀 있는 경우가 많고, 웹에서 가져온 내용은 출처의 신뢰도를 보장할 수 없습니다. 사내 안내 챗봇이라면 "찾을 수 없습니다. 담당자에게 문의해 주세요"로 답하는 편이 오히려 안전합니다.

검색 결과를 몇 개나 LLM에게 주는 것이 좋나요?

5개 안팎이 무난합니다. 3개면 필요한 내용이 빠질 수 있고, 20개를 넘기면 관련 없는 내용이 섞여 들어가 오히려 답변 품질이 떨어집니다. 리랭킹으로 잘 걸러 냈다면 5개로 충분합니다.

다음 편에서는 지금까지의 내용을 실제로 동작하는 챗봇으로 조립하는 방법을 다룹니다.

조회 41댓글 0 내려받기 통계
0

김영민 님의 다른 글

김영민 님 글 모아보기 →

댓글 0

첫 댓글을 남겨보세요.
댓글을 쓰려면 로그인이 필요합니다.