티스토리 뷰
작성일: 2026년 01월 19일
카테고리: AI, RAG, Vector Search
키워드: Embedding, Cosine Similarity, Vector Database, Hybrid Search, Full-text Search

요약
RAG 시스템의 검색 품질은 전체 시스템 성능을 결정한다. 이 글에서는 텍스트를 수학적 벡터로 변환하여 '의미'를 검색하는 벡터 유사도 검색의 메커니즘을 분석하고, 키워드 검색을 결합한 하이브리드 검색 전략을 다룬다.
검색기(Retriever)의 역할
RAG의 성패는 검색기가 얼마나 정확한 문서를 찾아오느냐에 달려 있다.
[좋은 검색] → 관련 문서 → 정확한 답변
[나쁜 검색] → 무관한 문서 → 부정확한 답변 또는 환각전통적인 키워드 검색은 단어의 정확한 일치를 찾는다. "자동차"를 검색하면 "자동차"가 포함된 문서만 반환하고, "차량"이나 "vehicle"은 찾지 못한다.
벡터 검색은 다르다. 단어가 아닌 의미를 검색한다.
임베딩: 텍스트를 숫자로 변환
임베딩이란
텍스트를 고차원 숫자 벡터로 변환하는 과정이다. 의미가 비슷한 텍스트는 비슷한 벡터를 갖는다.
from openai import OpenAI
client = OpenAI()
# 텍스트를 벡터로 변환
response = client.embeddings.create(
model="text-embedding-3-small",
input="자동차 정비 방법"
)
embedding = response.data[0].embedding
# [0.023, -0.041, 0.089, ...] (1,536차원)
임베딩의 특성
"자동차 정비 방법" → [0.023, -0.041, 0.089, ...]
"차량 수리 가이드" → [0.021, -0.039, 0.091, ...] ← 의미가 비슷하므로 벡터도 유사
"오늘 날씨가 좋다" → [0.156, 0.234, -0.078, ...] ← 의미가 다르므로 벡터도 다름이 특성 덕분에 키워드가 정확히 일치하지 않아도 의미적으로 관련된 문서를 찾을 수 있다.
임베딩 모델 선택 시 주의사항
임베딩 모델 일관성이 핵심이다.
graph TB
D1[문서 A] -->|모델 v1| E1[벡터 A]
D2[문서 B] -->|모델 v1| E2[벡터 B]
Q[질문] -->|모델 v2| EQ[질문 벡터]
E1 --> S[유사도 검색]
E2 --> S
EQ --> S
S --> X[결과 부정확]
style X stroke:#dc2626,stroke-width:2px
문서 임베딩과 질문 임베딩에 다른 모델을 사용하면 벡터 공간이 달라져 검색이 실패한다. 임베딩 모델을 변경하면 기존 인덱스를 모두 폐기하고 다시 구축해야 한다. 이는 높은 전환 비용을 수반한다.
텍스트 청킹: 검색 정밀도의 핵심
왜 청킹이 필요한가
100페이지 문서 전체를 하나의 벡터로 만들면 어떻게 될까?
문서: [서론 10페이지] + [본론 80페이지] + [결론 10페이지]
질문: "3장의 핵심 내용은?"
→ 전체 문서 벡터는 3장만의 의미를 정확히 담지 못함
→ 검색 실패 또는 부정확한 결과문서를 적절한 크기의 청크(Chunk)로 분할해야 정밀한 검색이 가능하다.
청킹 전략
1. 고정 크기 청킹
# 단순 분할 (비추천)
chunks = [text[i:i+500] for i in range(0, len(text), 500)]
문제: 단어나 문장 중간에서 잘릴 수 있다.
2. 슬라이딩 윈도우 청킹 (권장)
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 청크 크기
chunk_overlap=50, # 중첩 크기 (문맥 보존)
separators=["\n\n", "\n", " ", ""] # 분리 기준 우선순위
)
chunks = splitter.split_text(document)
중첩(Overlap)을 두면 청크 경계에서 문맥이 단절되는 것을 방지한다.
청크 1: [문장 A] [문장 B] [문장 C의 앞부분]
청크 2: [문장 C] [문장 D] [문장 E]
↑ 중첩 영역청크 크기 결정 기준
| 청크 크기 | 장점 | 단점 |
|---|---|---|
| 작음 (200자) | 검색 정밀도 높음 | 문맥 부족 |
| 큼 (2000자) | 풍부한 문맥 | 검색 정밀도 낮음 |
| 중간 (500-1000자) | 균형 | - |
일반적으로 500-1000자 범위에서 시작하고, 도메인에 맞게 조정한다.
유사도 계산: 코사인 유사도
코사인 유사도란
두 벡터 사이의 각도를 측정한다. 방향이 같으면 1, 직각이면 0, 반대면 -1이다.
벡터 A = [0.5, 0.5]
벡터 B = [0.4, 0.6]
벡터 C = [-0.5, -0.5]
cos(A, B) = 0.98 ← 매우 유사
cos(A, C) = -1.0 ← 반대 방향수식
cos(A, B) = (A · B) / (||A|| × ||B||)- A · B: 두 벡터의 내적 (dot product)
- ||A||: 벡터 A의 크기 (magnitude)
구현
import numpy as np
def cosine_similarity(vec_a, vec_b):
dot_product = np.dot(vec_a, vec_b)
norm_a = np.linalg.norm(vec_a)
norm_b = np.linalg.norm(vec_b)
return dot_product / (norm_a * norm_b)
# 사용
similarity = cosine_similarity(query_embedding, doc_embedding)
# 0.92 → 매우 유사
벡터 인덱싱: 대규모 검색의 핵심
문제: 브루트포스 검색의 한계
100만 개 문서가 있다면, 질문 하나에 100만 번의 유사도 계산이 필요하다.
문서 수: 1,000,000
벡터 차원: 1,536
계산량: 1,000,000 × 1,536 = 15억 연산실시간 서비스에서는 허용할 수 없는 지연이 발생한다.
해결: 근사 최근접 이웃 (ANN)
Approximate Nearest Neighbor 알고리즘은 정확도를 약간 포기하고 속도를 극대화한다.
graph TB
subgraph "Brute Force"
Q1[질문] --> ALL[모든 문서와 비교]
ALL --> R1[정확한 결과]
end
subgraph "ANN (HNSW)"
Q2[질문] --> IDX[인덱스 탐색]
IDX --> PART[후보군만 비교]
PART --> R2[근사 결과]
end
style R1 stroke:#16a34a,stroke-width:2px
style R2 stroke:#ea580c,stroke-width:2px
주요 ANN 알고리즘:
| 알고리즘 | 특징 |
|---|---|
| HNSW | 높은 정확도, 메모리 사용 많음 |
| IVF | 빠른 검색, 인덱스 구축 빠름 |
| PQ | 메모리 효율적, 정확도 낮음 |
대부분의 벡터 DB는 HNSW를 기본 인덱스로 사용한다.
벡터 DB 선택
# ChromaDB (로컬 개발)
import chromadb
client = chromadb.Client()
collection = client.create_collection("docs")
collection.add(documents=chunks, embeddings=embeddings)
# Pinecone (클라우드)
import pinecone
pinecone.init(api_key="...")
index = pinecone.Index("docs")
index.upsert(vectors=[(id, embedding, metadata)])
# Neo4j (그래프 + 벡터)
# Cypher로 벡터 검색
CALL db.index.vector.queryNodes('doc_embeddings', 5, $queryVector)
벡터 검색의 한계
벡터 검색은 의미적 유사성을 찾는 데 탁월하지만, 모든 검색 시나리오에 적합하지 않다.
문제 1: 고유 식별자 검색
질문: "주문번호 ORD-2024-12345의 상태는?"
벡터 검색: "주문", "상태" 관련 문서 반환
→ 해당 주문번호 문서를 찾지 못함문제 2: 정확한 키워드 매칭
질문: "ConfigMap 설정 오류"
벡터 검색: Kubernetes 설정 관련 문서들 반환
→ "ConfigMap"이 정확히 포함된 문서 누락 가능문제 3: 숫자/날짜 범위 검색
질문: "2024년 1월~3월 매출 보고서"
벡터 검색: "매출 보고서" 의미의 문서 반환
→ 날짜 범위 필터링 불가하이브리드 검색: 벡터 + 키워드
아키텍처
벡터 검색과 전체 텍스트 검색(Full-text Search)을 결합한다.
graph TB
Q[질문] --> V[벡터 검색]
Q --> K[키워드 검색]
V --> R1[의미 기반 결과]
K --> R2[키워드 기반 결과]
R1 --> M[결과 병합/재순위]
R2 --> M
M --> F[최종 결과]
style Q stroke:#2563eb,stroke-width:2px
style F stroke:#16a34a,stroke-width:2px
구현
from langchain.retrievers import EnsembleRetriever
from langchain.retrievers import BM25Retriever
from langchain_community.vectorstores import Chroma
# 벡터 리트리버
vector_retriever = Chroma.from_documents(docs, embeddings).as_retriever()
# 키워드 리트리버 (BM25)
keyword_retriever = BM25Retriever.from_documents(docs)
# 하이브리드 리트리버
hybrid_retriever = EnsembleRetriever(
retrievers=[vector_retriever, keyword_retriever],
weights=[0.5, 0.5] # 가중치 조절 가능
)
# 검색
results = hybrid_retriever.invoke("ORD-2024-12345 주문 상태")
가중치 전략
| 시나리오 | 벡터 가중치 | 키워드 가중치 |
|---|---|---|
| 일반 질문 | 0.7 | 0.3 |
| 기술 문서 (코드, ID 포함) | 0.4 | 0.6 |
| 법률/의료 (정확한 용어 중요) | 0.3 | 0.7 |
도메인에 따라 가중치를 조정한다.
BM25란
Best Matching 25는 전통적인 정보 검색 알고리즘이다. 단어 빈도(TF)와 역문서 빈도(IDF)를 기반으로 관련도를 계산한다.
BM25 점수 = TF 요소 × IDF 요소 × 문서 길이 정규화- 단어가 문서에 자주 등장하면 → 점수 증가
- 단어가 전체 문서에 드물게 등장하면 → 점수 증가 (희소성)
실습: ChromaDB로 하이브리드 검색
환경 설정
pip install chromadb langchain langchain-openai rank-bm25
전체 코드
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.retrievers import BM25Retriever, EnsembleRetriever
from langchain.schema import Document
# 1. 샘플 문서
documents = [
Document(page_content="주문번호 ORD-2024-12345는 배송 중입니다.", metadata={"type": "order"}),
Document(page_content="배송 상태를 확인하려면 마이페이지에서 조회하세요.", metadata={"type": "guide"}),
Document(page_content="주문번호 ORD-2024-67890은 결제 대기 중입니다.", metadata={"type": "order"}),
Document(page_content="배송 조회 서비스는 오전 9시부터 오후 6시까지 운영됩니다.", metadata={"type": "guide"}),
]
# 2. 벡터 리트리버
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(documents, embeddings)
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
# 3. BM25 리트리버
bm25_retriever = BM25Retriever.from_documents(documents)
bm25_retriever.k = 2
# 4. 하이브리드 리트리버
hybrid_retriever = EnsembleRetriever(
retrievers=[vector_retriever, bm25_retriever],
weights=[0.4, 0.6] # 주문번호 검색이므로 키워드 가중치 높임
)
# 5. 검색 테스트
query = "ORD-2024-12345 상태"
print("=== 벡터 검색 결과 ===")
for doc in vector_retriever.invoke(query):
print(f"- {doc.page_content}")
print("\n=== BM25 검색 결과 ===")
for doc in bm25_retriever.invoke(query):
print(f"- {doc.page_content}")
print("\n=== 하이브리드 검색 결과 ===")
for doc in hybrid_retriever.invoke(query):
print(f"- {doc.page_content}")
예상 출력
=== 벡터 검색 결과 ===
- 배송 상태를 확인하려면 마이페이지에서 조회하세요.
- 주문번호 ORD-2024-12345는 배송 중입니다.
=== BM25 검색 결과 ===
- 주문번호 ORD-2024-12345는 배송 중입니다.
- 주문번호 ORD-2024-67890은 결제 대기 중입니다.
=== 하이브리드 검색 결과 ===
- 주문번호 ORD-2024-12345는 배송 중입니다.
- 배송 상태를 확인하려면 마이페이지에서 조회하세요.하이브리드 검색이 정확한 주문번호를 1순위로 반환한다.
핵심 정리
| 개념 | 설명 |
|---|---|
| 임베딩 | 텍스트를 고차원 벡터로 변환 |
| 코사인 유사도 | 벡터 간 각도로 유사성 측정 |
| 청킹 | 문서를 검색 단위로 분할 |
| ANN 인덱스 | 대규모 데이터 빠른 검색 |
| 하이브리드 검색 | 벡터 + 키워드 검색 결합 |
다음 단계
기본적인 벡터 검색과 하이브리드 검색을 다뤘다. 다음 글에서는 검색 품질을 더욱 높이기 위한 고급 벡터 검색 전략을 분석한다. Step-back Prompting, Parent Document Retriever 등 실무에서 활용되는 기법을 다룬다.
시리즈 목차
- LLM 정확도 향상
- 벡터 검색과 하이브리드 검색 (현재 글)
- 고급 벡터 검색 전략
- Text2Cypher: 자연어를 그래프 쿼리로
- Agentic RAG
- LLM으로 지식 그래프 구축
- Microsoft GraphRAG 구현
- RAG 평가 체계
참고 자료
공식 문서
학술 자료
- Robertson & Zaragoza (2009). "The Probabilistic Relevance Framework: BM25 and Beyond"
'실제 경험과 인사이트를 AI와 함께 정리한 글' 카테고리의 다른 글
| Essential GraphRAG Part 4: Text2Cypher - 자연어를 그래프 쿼리로 (0) | 2026.01.19 |
|---|---|
| Essential GraphRAG Part 3: 고급 벡터 검색 전략 (0) | 2026.01.19 |
| Essential GraphRAG Part 1: LLM 정확도 향상 (0) | 2026.01.19 |
| Essential GraphRAG 시리즈: 벡터 검색에서 에이전틱 RAG까지 (0) | 2026.01.19 |
| Agent Skills로 Claude Code 확장하기: React 개발 역량 강화 (0) | 2026.01.16 |
- Total
- Today
- Yesterday
- Kubernetes
- PYTHON
- AI
- LLM
- Tailwind CSS
- Claude Opus 4.5
- backend
- Ontology
- architecture
- Development Tools
- Developer Tools
- Tax Analysis
- troubleshooting
- AI Development
- Rag
- Next.js
- claude code
- authorization
- authentication
- react
- AI agent
- Go
- frontend
- api gateway
- knowledge graph
- AGENTS.md
- 개발 도구
- SHACL
- imprun.dev
- security
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
