RAG 시스템 벡터 데이터베이스 가이드
RAG 시스템 벡터 데이터베이스 구축 완벽 가이드
AI 챗봇이 "우리 회사 문서"를 읽고 대답하게 만드는 기술, 처음부터 끝까지 알려드립니다.
들어가며: 왜 RAG가 필요한가?
ChatGPT에게 "우리 회사 휴가 규정이 뭐야?"라고 물어본 적 있으신가요? 당연히 모릅니다. ChatGPT는 우리 회사 내부 문서를 학습한 적이 없으니까요.
그렇다면 방법은 두 가지입니다.
첫 번째, AI 모델을 우리 회사 데이터로 다시 학습시킨다(Fine-tuning). 하지만 이 방법은 비용이 수천만 원 이상 들고, 문서가 바뀔 때마다 재학습이 필요합니다.
두 번째, 질문할 때마다 관련 문서를 찾아서 AI에게 함께 건네준다. 이것이 바로 RAG입니다.
RAG(Retrieval-Augmented Generation)는 직역하면 "검색으로 보강된 생성"입니다. 사용자가 질문하면, 먼저 관련 문서를 검색(Retrieval) 하고, 그 문서를 참고자료로 첨부(Augmented) 해서 AI가 답변을 생성(Generation) 하는 구조입니다.
쉽게 비유하면 이렇습니다. 오픈북 시험을 떠올려 보세요. 학생(AI)이 모든 내용을 외울 필요 없이, 시험 문제(사용자 질문)가 나오면 교과서(문서 데이터베이스)에서 해당 부분을 찾아 읽고 답을 적는 것입니다.
1. RAG 시스템의 전체 구조 이해하기
RAG 시스템은 크게 두 단계로 나뉩니다.
1단계: 사전 준비 (Indexing)
문서를 미리 잘게 쪼개서 벡터 데이터베이스에 저장해 두는 과정입니다. 이 과정은 사용자 질문이 들어오기 전에 미리 수행됩니다.
[원본 문서] → [텍스트 추출] → [청크 분할] → [임베딩 변환] → [벡터 DB 저장]
2단계: 질의 응답 (Query & Generation)
사용자가 질문하면 관련 문서를 찾아 AI에게 전달하는 과정입니다.
[사용자 질문] → [질문 임베딩] → [벡터 DB 검색] → [관련 문서 추출] → [LLM에 전달] → [답변 생성]
이 두 단계를 이해하면 RAG의 80%는 파악한 것입니다. 이제 각 단계를 하나씩 깊이 살펴보겠습니다.
2. 핵심 개념: 임베딩(Embedding)이란?
벡터 데이터베이스를 이해하려면 먼저 임베딩을 알아야 합니다.
컴퓨터는 텍스트를 모른다
컴퓨터는 본질적으로 숫자만 이해합니다. "고양이"라는 단어를 컴퓨터에게 이해시키려면 숫자로 바꿔야 합니다. 임베딩은 텍스트를 숫자 배열(벡터)로 변환하는 기술입니다.
"고양이" → [0.21, -0.45, 0.89, 0.12, ..., -0.33] (1536개의 숫자)
"강아지" → [0.19, -0.41, 0.85, 0.15, ..., -0.28] (1536개의 숫자)
"자동차" → [-0.72, 0.33, -0.11, 0.67, ..., 0.54] (1536개의 숫자)
여기서 중요한 점은 의미가 비슷한 단어는 비슷한 숫자 배열을 갖는다는 것입니다. "고양이"와 "강아지"의 벡터는 서로 가깝고, "자동차"의 벡터는 둘과 멀리 떨어져 있습니다.
이것을 수학적으로 표현하면 **"고차원 공간에서 의미적으로 유사한 텍스트가 가까운 위치에 매핑된다"**고 합니다. 하지만 직관적으로는 그냥 "비슷한 의미 = 비슷한 숫자"라고 이해하면 충분합니다.
임베딩 모델 선택하기
임베딩을 생성하려면 임베딩 모델이 필요합니다. 주요 선택지를 정리하면 다음과 같습니다.
OpenAI text-embedding-3-small — 가장 쉽게 시작할 수 있는 선택입니다. API 호출 한 번으로 임베딩이 생성되고, 1536차원 벡터를 반환합니다. 비용은 100만 토큰당 약 $0.02로 매우 저렴합니다. 처음 RAG를 구축한다면 이것부터 시작하는 걸 추천합니다.
OpenAI text-embedding-3-large — small 모델의 상위 버전으로, 3072차원 벡터를 반환합니다. 더 정밀한 검색이 필요할 때 사용하지만, 대부분의 경우 small 모델로도 충분합니다.
Sentence-Transformers (오픈소스) — 로컬에서 무료로 실행할 수 있는 오픈소스 모델입니다. all-MiniLM-L6-v2가 대표적이며, 384차원 벡터를 반환합니다. API 비용 없이 사용할 수 있지만, 한국어 성능은 상대적으로 약합니다.
다국어/한국어 특화 모델 — 한국어 데이터를 다룬다면 intfloat/multilingual-e5-large이나 BAAI/bge-m3 같은 다국어 모델을 고려해 볼 수 있습니다. 한국어 텍스트의 의미를 더 정확하게 포착합니다.
3. 벡터 데이터베이스란?
일반적인 데이터베이스(MySQL, PostgreSQL 등)는 정확한 값을 찾는 데 최적화되어 있습니다. WHERE name = '홍길동'처럼 정확히 일치하는 데이터를 찾죠.
벡터 데이터베이스는 다릅니다. **"이것과 비슷한 것을 찾아줘"**라는 질문에 답하도록 설계되었습니다. 수백만 개의 벡터 중에서 주어진 벡터와 가장 가까운 벡터를 빠르게 찾아주는 것이 핵심 기능입니다.
유사도 검색의 원리
두 벡터가 얼마나 비슷한지 측정하는 방법은 주로 두 가지가 있습니다.
코사인 유사도(Cosine Similarity) — 두 벡터가 이루는 각도를 측정합니다. 1에 가까우면 유사하고, 0에 가까우면 관련 없으며, -1에 가까우면 반대 의미입니다. 가장 널리 사용되는 방법이며, 텍스트 검색에서는 보통 이 방법을 사용합니다.
유클리드 거리(Euclidean Distance) — 두 점 사이의 직선 거리를 측정합니다. 값이 작을수록 유사합니다. 직관적이지만, 고차원 벡터에서는 코사인 유사도보다 성능이 떨어질 수 있습니다.
주요 벡터 데이터베이스 비교
현재 시장에는 다양한 벡터 데이터베이스가 있습니다. 각각의 특징을 살펴보겠습니다.
Chroma — 로컬에서 pip install chromadb 한 줄로 바로 시작할 수 있습니다. 파이썬 개발자에게 가장 친숙하며, 학습용이나 프로토타입 개발에 최적입니다. 단, 대규모 프로덕션 환경에서는 한계가 있습니다.
Pinecone — 완전 관리형 클라우드 서비스입니다. 서버 관리가 필요 없고, 무료 플랜으로 시작할 수 있습니다. 빠르게 프로덕션 레벨의 RAG를 구축하고 싶다면 좋은 선택입니다. 다만 데이터가 외부 클라우드에 저장되므로 보안 민감한 데이터에는 주의가 필요합니다.
Weaviate — 자체 호스팅과 클라우드 모두 지원합니다. 하이브리드 검색(벡터 + 키워드)을 기본 지원하며, GraphQL API를 제공합니다. 유연한 스키마 설정이 가능해서 복잡한 데이터 구조에 적합합니다.
Milvus — 대규모 벡터 검색에 특화된 오픈소스 데이터베이스입니다. 수억 건 이상의 벡터를 다뤄야 하는 대규모 시스템에 적합합니다. Docker로 쉽게 배포할 수 있지만, 설정의 복잡도가 높은 편입니다.
pgvector (PostgreSQL 확장) — 이미 PostgreSQL을 사용하고 있다면 가장 실용적인 선택입니다. 기존 DB에 확장만 추가하면 벡터 검색을 사용할 수 있고, 일반 SQL과 벡터 검색을 하나의 쿼리에서 결합할 수 있습니다. Spring Boot 프로젝트에서 JPA와 함께 쓰기에도 좋습니다.
초급자를 위한 추천: 학습 목적이라면 Chroma로 시작하세요. 프로덕션 서비스를 빠르게 만들어야 한다면 Pinecone이나 pgvector를 추천합니다.
4. 실전: 벡터 데이터베이스 구축하기
이제 실제로 코드를 작성해 보겠습니다. Python과 LangChain을 사용해 가장 기본적인 RAG 파이프라인을 구축합니다.
4-1. 환경 설정
pip install langchain langchain-openai langchain-community chromadb tiktoken
import os
os.environ["OPENAI_API_KEY"] = "sk-your-api-key-here"
4-2. 문서 로딩
RAG의 첫 단계는 문서를 불러오는 것입니다. LangChain은 다양한 형식의 문서 로더를 제공합니다.
from langchain_community.document_loaders import (
TextLoader,
PyPDFLoader,
CSVLoader,
WebBaseLoader
)
# 텍스트 파일 로딩
loader = TextLoader("company_rules.txt", encoding="utf-8")
documents = loader.load()
# PDF 파일 로딩
loader = PyPDFLoader("employee_handbook.pdf")
documents = loader.load()
# 웹 페이지 로딩
loader = WebBaseLoader("https://example.com/faq")
documents = loader.load()
print(f"로딩된 문서 수: {len(documents)}")
print(f"첫 번째 문서 미리보기: {documents[0].page_content[:200]}")
각 document 객체에는 두 가지 핵심 속성이 있습니다. page_content에는 실제 텍스트가, metadata에는 파일명, 페이지 번호 같은 부가 정보가 담겨 있습니다.
4-3. 청크 분할 (Chunking)
문서 전체를 하나의 벡터로 만들면 검색 정밀도가 떨어집니다. "사내 휴가 규정"에 대해 질문했는데 50페이지짜리 사원 핸드북 전체가 반환되면 AI가 답변을 만들기 어렵습니다. 그래서 문서를 작은 단위(청크)로 나눠야 합니다.
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 각 청크의 최대 글자 수
chunk_overlap=50, # 청크 간 겹치는 글자 수
separators=["\n\n", "\n", ".", " ", ""] # 분할 기준 (우선순위 순)
)
chunks = text_splitter.split_documents(documents)
print(f"원본 문서 수: {len(documents)}")
print(f"분할된 청크 수: {len(chunks)}")
print(f"첫 번째 청크: {chunks[0].page_content}")
청크 분할에서 자주 하는 실수와 해결법:
chunk_size가 너무 크면(2000자 이상) 검색 정밀도가 떨어지고, 너무 작으면(100자 이하) 문맥이 잘려서 AI가 제대로 이해하지 못합니다. 한국어 기준으로는 300~800자 사이가 적당합니다.
chunk_overlap은 청크 경계에서 문맥이 끊기는 것을 방지합니다. 예를 들어 "연차는 입사 1년 후부터 / 15일이 부여됩니다"가 두 청크로 쪼개지면 의미가 깨집니다. 겹침을 설정하면 양쪽 청크 모두에 완전한 문장이 포함됩니다.
separators는 문서를 어디서 끊을지 결정합니다. RecursiveCharacterTextSplitter는 첫 번째 구분자(\n\n, 문단)로 먼저 시도하고, 청크가 여전히 크면 다음 구분자(\n, 줄바꿈)로 시도하는 식으로 점점 작은 단위로 내려갑니다.
4-4. 임베딩 생성 & 벡터 DB 저장
이제 청크를 벡터로 변환하고 데이터베이스에 저장합니다.
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
# 임베딩 모델 초기화
embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")
# 벡터 DB 생성 및 저장 (이 한 줄이 임베딩 변환 + 저장을 모두 수행)
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embedding_model,
persist_directory="./chroma_db", # 로컬 저장 경로
collection_name="company_docs" # 컬렉션 이름
)
print(f"벡터 DB에 {len(chunks)}개의 청크가 저장되었습니다.")
내부적으로 일어나는 일: Chroma.from_documents()를 호출하면 LangChain이 각 청크의 텍스트를 OpenAI 임베딩 API에 보내 벡터로 변환하고, 그 벡터와 원본 텍스트, 메타데이터를 함께 Chroma DB에 저장합니다.
4-5. 유사도 검색 테스트
저장이 완료되면 검색이 제대로 되는지 테스트해 봅니다.
# 기존 DB 불러오기 (서버 재시작 후에도 사용 가능)
vectorstore = Chroma(
persist_directory="./chroma_db",
embedding_function=embedding_model,
collection_name="company_docs"
)
# 유사도 검색
query = "연차 휴가는 며칠인가요?"
results = vectorstore.similarity_search_with_score(query, k=3)
for doc, score in results:
print(f"[유사도: {score:.4f}]")
print(f"내용: {doc.page_content[:150]}")
print(f"출처: {doc.metadata}")
print("---")
k=3은 가장 유사한 상위 3개 청크를 가져오라는 뜻입니다. score는 유사도 점수로, Chroma의 경우 거리(distance)를 반환하므로 값이 작을수록 더 유사합니다.
4-6. RAG 체인 완성
검색 결과를 LLM에 전달해서 최종 답변을 생성하는 단계입니다.
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.schema.runnable import RunnablePassthrough
from langchain.schema.output_parser import StrOutputParser
# LLM 초기화
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# 프롬프트 템플릿
prompt = ChatPromptTemplate.from_template("""
아래의 참고 문서를 기반으로 질문에 답변해 주세요.
참고 문서에 없는 내용은 "해당 정보를 찾을 수 없습니다"라고 답변해 주세요.
## 참고 문서
{context}
## 질문
{question}
## 답변
""")
# 검색기(Retriever) 설정
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 3}
)
# RAG 체인 구성
rag_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# 질문하기
answer = rag_chain.invoke("신입사원의 연차 휴가는 며칠인가요?")
print(answer)
이것이 가장 기본적인 RAG 파이프라인의 전체 코드입니다. 문서 로딩부터 최종 답변 생성까지, 50줄 안팎의 코드로 구현할 수 있습니다.
5. Spring Boot에서 RAG 구축하기
Java/Spring Boot 환경에서 RAG를 구축하는 방법도 살펴보겠습니다. Spring AI 프레임워크를 활용하면 스프링 생태계 안에서 자연스럽게 RAG를 구현할 수 있습니다.
5-1. 의존성 추가
<!-- build.gradle 또는 pom.xml -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
</dependency>
5-2. 설정
# application.yml
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
embedding:
options:
model: text-embedding-3-small
vectorstore:
pgvector:
index-type: HNSW
distance-type: COSINE_DISTANCE
dimensions: 1536
5-3. 문서 저장 서비스
@Service
@RequiredArgsConstructor
public class DocumentIngestionService {
private final VectorStore vectorStore;
public void ingestDocument(String filePath) {
// 1. 문서 로딩
var loader = new PagePdfDocumentReader(new FileSystemResource(filePath));
List<Document> documents = loader.get();
// 2. 청크 분할
var splitter = new TokenTextSplitter(500, 50, 5, 10000, true);
List<Document> chunks = splitter.apply(documents);
// 3. 벡터 DB 저장 (임베딩 변환은 자동으로 수행됨)
vectorStore.add(chunks);
log.info("{}개의 청크가 벡터 DB에 저장되었습니다.", chunks.size());
}
}
5-4. RAG 질의 서비스
@Service
@RequiredArgsConstructor
public class RagQueryService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
public String ask(String question) {
// 1. 관련 문서 검색
List<Document> relevantDocs = vectorStore.similaritySearch(
SearchRequest.builder()
.query(question)
.topK(3)
.build()
);
// 2. 컨텍스트 조합
String context = relevantDocs.stream()
.map(Document::getText)
.collect(Collectors.joining("\n\n"));
// 3. LLM에 전달하여 답변 생성
String prompt = """
아래의 참고 문서를 기반으로 질문에 답변해 주세요.
참고 문서에 없는 내용은 "해당 정보를 찾을 수 없습니다"라고 답변해 주세요.
## 참고 문서
%s
## 질문
%s
""".formatted(context, question);
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
}
6. 검색 품질을 높이는 실전 팁
기본 RAG를 구축했다면, 이제 품질을 끌어올릴 차례입니다.
팁 1: 하이브리드 검색 적용하기
벡터 검색만으로는 한계가 있습니다. "이사회 결의문 제2023-015호"처럼 정확한 키워드가 중요한 경우, 벡터 검색은 의미적으로 유사한 다른 문서를 가져올 수 있습니다.
하이브리드 검색은 벡터 검색(의미 기반)과 키워드 검색(BM25)을 결합합니다. 이렇게 하면 의미적 유사성과 키워드 정확도를 동시에 잡을 수 있습니다.
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
# 키워드 기반 검색기
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 3
# 벡터 기반 검색기
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# 하이브리드 검색기 (가중치: 벡터 60%, 키워드 40%)
hybrid_retriever = EnsembleRetriever(
retrievers=[vector_retriever, bm25_retriever],
weights=[0.6, 0.4]
)
results = hybrid_retriever.invoke("이사회 결의문 제2023-015호")
팁 2: 메타데이터 필터링 활용하기
문서에 메타데이터를 잘 설정해 두면 검색 범위를 좁힐 수 있습니다.
from langchain.schema import Document
# 메타데이터가 포함된 청크 생성
doc = Document(
page_content="신입사원은 입사 1년차에 11일의 연차휴가가 부여됩니다.",
metadata={
"source": "employee_handbook.pdf",
"department": "인사팀",
"category": "휴가규정",
"updated_at": "2024-01-15",
"page": 23
}
)
# 메타데이터 기반 필터 검색
results = vectorstore.similarity_search(
query="연차 휴가 규정",
k=3,
filter={"category": "휴가규정"} # 휴가규정 카테고리만 검색
)
팁 3: 검색 결과 재순위화 (Reranking)
벡터 검색으로 후보군을 넓게 가져온 후, 더 정밀한 모델로 재순위를 매기는 방법입니다. 검색 품질을 한 단계 끌어올리는 강력한 기법입니다.
# Cohere Reranker 사용 예시
from langchain.retrievers import ContextualCompressionRetriever
from langchain_cohere import CohereRerank
# 1차: 벡터 검색으로 후보 10개 추출
base_retriever = vectorstore.as_retriever(search_kwargs={"k": 10})
# 2차: Reranker로 상위 3개 재선별
reranker = CohereRerank(model="rerank-v3.5", top_n=3)
retriever = ContextualCompressionRetriever(
base_compressor=reranker,
base_retriever=base_retriever
)
results = retriever.invoke("신입사원 휴가 규정")
팁 4: 청크에 요약 정보 추가하기
각 청크에 상위 문서의 제목이나 요약을 함께 저장하면 검색 품질이 올라갑니다. 이렇게 하면 청크 단독으로는 문맥이 부족할 때 보완이 됩니다.
for chunk in chunks:
# 원본 문서 제목과 섹션 정보를 청크 앞에 추가
chunk.page_content = (
f"[문서: {chunk.metadata['source']}] "
f"[섹션: {chunk.metadata.get('section', '기타')}]\n"
f"{chunk.page_content}"
)
7. 프로덕션 체크리스트
실제 서비스에 RAG를 배포할 때 고려해야 할 사항들입니다.
성능 최적화 — 벡터 DB의 인덱스 타입을 적절히 선택해야 합니다. HNSW(Hierarchical Navigable Small World)는 검색 속도와 정확도의 균형이 가장 좋아 대부분의 경우에 추천됩니다. 데이터가 100만 건 이상이면 IVF(Inverted File Index) 계열을 고려해 보세요.
비용 관리 — 임베딩 API 호출 비용을 미리 계산하세요. 문서 10만 건, 평균 500토큰이면 OpenAI 기준 약 $1 정도입니다. 검색 시점의 질문 임베딩 비용은 미미하지만, 대량의 문서를 새로 임베딩할 때는 배치 처리로 비용을 절약하세요.
데이터 동기화 — 원본 문서가 수정되면 벡터 DB도 업데이트해야 합니다. 문서별로 고유 ID를 부여하고, 변경된 문서의 청크만 삭제 후 재삽입하는 증분 업데이트 전략을 사용하세요.
모니터링 — 사용자 질문과 검색된 청크, 최종 답변을 로깅하세요. 검색 결과가 질문과 관련 없는 경우(Low Relevance)를 추적하면 시스템을 지속적으로 개선할 수 있습니다.
보안 — 사내 문서를 다루는 경우 데이터가 외부로 유출되지 않도록 주의해야 합니다. 자체 호스팅 벡터 DB(Milvus, pgvector)를 사용하거나, 클라우드 서비스의 데이터 처리 정책을 반드시 확인하세요.
마치며
RAG 시스템의 핵심을 정리하면 이렇습니다. 문서를 잘게 나누고, 벡터로 변환해서 저장하고, 질문이 들어오면 비슷한 문서를 찾아서 AI에게 함께 전달한다. 이것이 전부입니다.
처음에는 Chroma + OpenAI 임베딩으로 간단하게 시작하세요. 동작하는 프로토타입을 먼저 만들고, 그 다음에 하이브리드 검색, 리랭킹, 메타데이터 필터링 같은 고급 기법을 하나씩 적용해 나가면 됩니다.
완벽한 설계를 고민하느라 시작을 미루지 마세요. RAG의 가장 좋은 점은 각 단계를 독립적으로 교체하고 개선할 수 있다는 것입니다. 벡터 DB를 Chroma에서 pgvector로 바꿔도 되고, 임베딩 모델을 OpenAI에서 오픈소스로 교체해도 됩니다. 먼저 만들고, 그 다음에 개선하세요.
이 글이 도움이 되셨다면 공유해 주세요. 다음 글에서는 RAG의 성능을 평가하는 방법(RAGAS 프레임워크)에 대해 다뤄보겠습니다.
댓글
댓글 쓰기