RAG 검색 파이프라인
벡터 검색(KNN/ANN), 하이브리드 검색
RAG 검색 파이프라인
벡터 검색(KNN/ANN), 하이브리드 검색
검색이 생성을 완성한다
Retrieval-Augmented Generation — LLM의 한계를 외부 지식으로 보완
검색 파이프라인
벡터 검색 · 유사도 메트릭 · KNN vs ANN
검색 파이프라인
질문을 벡터로 바꿔 유사 문서를 찾는 온라인 구간 — 매 요청 실시간 실행
검색 파이프라인 5단계
인덱싱과 달리 사용자 요청마다 실행됩니다 — 각 단계의 선택이 정확도·속도·비용을 결정
- 쿼리 임베딩
- 벡터 검색 (ANN)
- 하이브리드 검색 (선택)
- 리랭킹
- Top-K 선택
매 요청 실행되므로 지연 시간이 핵심입니다 — 그리고 쿼리 임베딩은 반드시 인덱싱과 동일한 모델을 사용합니다.
기존 검색 vs RAG의 검색
같은 "검색"이지만 찾는 방식이 다릅니다 — 단어 일치가 아니라 의미 거리
키워드 검색 (기존)
단어 일치 — 역색인·BM25
- "예약금"으로 찾으면 "예약금"이 있는 문서만
- "보증금", "선결제" 같은 다른 표현은 놓침
- 고유명사·코드·정확한 문구 매칭에는 강함
- 수십 년 검증된 방식 — 빠르고 예측 가능
검색엔진·DB의 LIKE 검색이 여기에 속합니다 — 단어가 같아야 찾습니다.
시맨틱 검색 (RAG)
의미 거리 — 임베딩 벡터 유사도
- 질문과 청크를 같은 좌표계의 벡터로 비교
- "예약금 환불"과 "보증금 반환 규정"이 매칭
- 표현이 달라도 의미가 같으면 찾아냄
- 인덱싱에서 만들어 둔 벡터가 전제 조건
앞 파트의 임베딩 좌표에서 질문과 가까운 점을 찾는 것 — RAG 검색의 본질입니다.
RAG의 검색은 단어 일치가 아니라 의미 거리 계산입니다 — 이걸 가능하게 하려고 인덱싱에서 모든 청크를 벡터로 만들어 두었습니다.
벡터 검색 동작 원리
키워드는 완전 일치만, 벡터는 동의어·유사 표현·다국어까지 포착
-
1
쿼리 임베딩
질문을 벡터로 변환 — 인덱싱과 동일한 임베딩 모델 필수
-
2
벡터 검색 (ANN)
수백만 벡터에서 유사 후보 수백 개를 밀리초에 탐색
-
3
하이브리드 (선택)
키워드(BM25) 결과와 결합 — 고유명사·코드 매칭 보완
-
4
리랭킹
후보를 쿼리와 1:1 정밀 비교하여 순위 재조정
-
5
Top-K 선택
최종 상위 K개만 LLM 프롬프트에 주입
벡터가 의미를 잡고 키워드가 고유명사를 잡습니다 — 실무 기본값은 하이브리드 + 리랭킹 조합입니다.
검색의 실제 — 유사도 점수와 Top-K
질문이 벡터가 되어 인덱스를 만나는 순간 — 점수로 줄 세우고 상위 K개만 통과
"예약금 환불 규정이 어떻게 되나요?"
→
[ 0.08, −0.62, 0.31, … ]
인덱스의 청크 벡터와 코사인 유사도
1등이 인덱싱에서 만든 청크 #12입니다 — 인덱싱 품질이 곧 검색 품질입니다. Top-K를 통과한 청크만 프롬프트에 들어갑니다.
유사도 메트릭 3가지
벡터 간 "가까움"을 측정하는 방법 — 데이터 특성에 따라 선택
코사인 유사도 추천
방향(각도)만 비교
- 범위 -1 ~ 1 — 1이면 같은 방향
- 벡터 크기와 무관 — 텍스트 길이에 강건
- OpenSearch에선 spaceType cosinesimil로 지정
언제 — 텍스트 검색의 표준
유클리드 거리
두 점 사이 직선 거리
- 범위 0 ~ 무한대 — 작을수록 유사
- 방향과 크기를 모두 반영
- 좌표·수치 데이터에 자연스러움
언제 — 이미지, 좌표 기반 데이터
내적 (Dot Product)
방향 + 크기 동시 반영
- 정규화된 벡터에선 코사인과 동일
- 계산이 가장 빠름
- 성능 최적화 경로로 활용
언제 — 대규모 검색의 성능 최적화
코사인은 방향이지 거리가 아닙니다 — 거리가 가까워도 방향이 다를 수 있으니 유클리드와 혼동하지 않습니다.
KNN — 모두와 비교하는 전수 검색
k-Nearest Neighbors — 질문 벡터를 인덱스의 모든 벡터와 비교해 가장 가까운 k개를 고른다
O(N) — 문서 수에 비례해 느려짐
정확도 100% — 진짜 최근접 보장
약 1만 건 미만이면 이걸로 충분
정답이 보장되는 대신 문서가 늘어난 만큼 그대로 느려집니다 — 매 질문마다 전체와 비교하기 때문입니다.
ANN — 지름길로 좁히는 근사 검색
Approximate Nearest Neighbor — 미리 만든 인덱스 구조로 후보를 좁혀 일부만 비교, "거의 정확한 답"을 밀리초에
HNSW 추천
Hierarchical Navigable Small World · 그래프 계열
- 계층 그래프를 위에서 아래로 좁혀 탐색
- O(log N) — 실시간 삽입에 강함
언제 — 프로덕션 표준 — OpenSearch 기본
IVF
Inverted File Index · 클러스터 계열
- 벡터를 군집으로 묶고 가까운 군집만 탐색
- O(√N) — 인덱스 구축이 빠르고 가벼움
언제 — 수억 벡터 배치 — FAISS
PQ
Product Quantization · 압축 계열
- 벡터를 짧은 코드로 압축해 메모리 절약
- 단독보다 IVF+PQ 조합으로 활용
언제 — 메모리가 병목일 때
LSH
Locality-Sensitive Hashing · 해시 계열
- 비슷한 벡터가 같은 버킷에 걸리는 해시
- 정확도가 낮아 RAG에선 드묾
언제 — 중복 탐지 · 초고속 근사
프로덕션 RAG의 표준은 HNSW입니다 — 나머지는 규모·메모리 제약이 있을 때 검토합니다.
KNN vs ANN — 정확도와 속도의 거래
KNN(k-Nearest Neighbors)은 정확하지만 느린 전수 비교 — ANN(Approximate Nearest Neighbor)은 인덱스로 "거의 정확한 답"을 밀리초에
| 방식 | 속도 | 정확도 | 메모리 | 적합 상황 |
|---|---|---|---|---|
| KNN (전수 비교) | O(N) — 느림 | 100% | 낮음 | 약 1만 건 미만, 정확도 절대 필요 |
| ANN — HNSW (그래프) | O(log N) — 매우 빠름 | 높음 (근사) | 높음 (그래프 상주) | 프로덕션 RAG, 실시간 삽입 |
| ANN — IVF (클러스터) | O(√N) — 빠름 | 중간~높음 (근사) | 중간 | 수억 벡터, 비용 우선 |
프로덕션은 ANN이 필수입니다 — OpenSearch 기본은 HNSW, 초대규모 배치는 FAISS의 IVF+PQ 조합을 검토합니다.
ANN의 속 — HNSW
계층 그래프를 위에서 아래로 좁혀 내려가는 탐색 — 전수 비교 없이 밀리초를 만드는 구조
노드가 적은 층에서 크게 건너뛰며 Q 근처로 이동합니다.
층 안에서 더 가까운 이웃이 없으면 아래층으로 내려가 더 촘촘하게 좁힙니다.
모든 벡터가 있는 Layer 0에서 최근접 이웃을 확정합니다.
정확도와 속도의 트레이드오프는 파라미터로 조절합니다 — OpenSearch 실습에서 HNSW 파라미터를 직접 만집니다.
ANN의 속 — IVF
Inverted File Index — 군집 대표만 먼저 비교하고 가까운 군집만 여는 탐색, 후보 수를 줄이는 지렛대
벡터 전체를 K개 군집으로 나누고, 군집마다 대표(센트로이드)를 둡니다.
질문 Q를 센트로이드 K개와만 비교해 가까운 군집 nprobe개를 고릅니다.
선택된 군집의 벡터만 비교 — 나머지는 통째로 건너뜁니다.
인덱스 구축이 빠르고 가벼워 수억 벡터 배치(FAISS)에 강합니다 — 군집 경계 근처의 이웃은 놓칠 수 있어 nprobe로 보정합니다.
ANN의 속 — PQ
Product Quantization — 벡터를 짧은 코드로 바꿔 압축한 채 비교, 메모리를 줄이는 지렛대
1,024차원을 128차원씩 8조각으로 나눕니다.
조각마다 대표 벡터 256개짜리 코드북에서 가장 가까운 ID(1바이트)로 바꿉니다.
대표끼리의 거리를 미리 계산한 표를 조합 — 복원 없이 압축된 채 비교합니다.
단독보다 IVF+PQ 조합이 정석입니다 — IVF가 후보를 줄이고, PQ가 그 후보를 압축한 채 비교합니다. 프로덕션 RAG 기본은 여전히 HNSW입니다.
증강의 실제 — 프롬프트에 근거 넣기
Top-K로 고른 청크가 프롬프트에 들어가는 순간 — R·A·G의 A는 결국 프롬프트 설계
러닝 예제
results = 검색을 통과한 청크 2개 — [문서 1] "예약금은 이용일 3일 전까지 취소하면 전액 환불…" (청크 #12)
question = "예약금 환불 규정이 어떻게 되나요?"
# ① 검색 결과(results)를 [문서 N]·출처 URI가 붙은 근거 블록으로 조립
context = "\n\n".join(
f"[문서 {i+1}] (출처: {r['location']['s3Location']['uri']})\n"
f"{r['content']['text']}"
for i, r in enumerate(results) # ② results는 Top-K개 — 컨텍스트 예산
)
# ③ 거절 지시 — 2행 / ④ 출처 표기 지시 — 3행
prompt = f"""아래 <근거> 문서만 사용해 질문에 답하세요.
근거에 없는 내용은 "문서에서 찾을 수 없습니다"라고 답합니다.
답변 끝에 사용한 문서 번호를 표기하세요.
<근거>
{context}
</근거>
질문: {question}"""
- 근거 블록 구분 — <근거> 태그·문서 번호로 경계를 명확히. 모델이 근거와 질문을 혼동하지 않게
- 컨텍스트 예산 — Top-K와 청크 크기의 곱이 프롬프트 비용. 넣을수록 좋은 게 아님
- 거절 지시 — "근거에 없으면 없다고 답하라". 환각을 줄이는 가장 싼 장치
- 출처 표기 지시 — 문서 번호·URI를 답변에 인용하게. Citation의 원리
인덱싱과 검색 파이프라인을 이해해야 올바른 RAG를 설계합니다.