
【해설】 실제 값을 바탕으로 벡터화 등 RAG의 메커니즘을 근본부터 이해하기
요약
RAG(검색 확장 생성)의 개념부터 데이터 전처리, 벡터화, 검색, 생성에 이르는 전체 메커니즘을 근본부터 상세히 설명합니다. 단순한 문서 검색을 넘어 벡터 DB 활용과 하이브리드 검색 등 RAG 시스템의 구성 요소를 심도 있게 다룹니다.
핵심 포인트
- RAG는 모델 가중치를 수정하지 않고 외부 지식을 컨텍스트로 활용하는 방식임
- 데이터 로드, 청킹, 벡터화, 벡터 DB 저장으로 이어지는 사전 준비 공정의 중요성
- 질문 벡터화, 유사도 계산, 재순위화(Re-ranking)를 포함하는 검색 프로세스
- RAG의 품질은 생성 모델뿐만 아니라 검색 단계의 정확도에 의해 결정됨
안녕하세요, miruky입니다.
생성형 AI (Generative AI)를 업무에서 사용하려고 하면, 상당히 높은 확률로 RAG라는 단어를 마주하게 됩니다. RAG는 Retrieval-Augmented Generation의 약자로, 일본어로는 검색 확장 생성(検索拡張生成)이라고 불리는 경우가 많습니다.
다만, RAG라는 단어만 들으면 왠지 모르게 "사내 문서를 검색해서 LLM에 전달하는 것" 정도의 이해에서 멈춰버리기 쉽지 않나요? 저의 경우, 벡터화 (Vectorization)나 검색 단계의 실제 이미지가 떠오르지 않아 RAG에 관해서는 막연하게만 이해하고 있었습니다. 하지만 어쩔 수 없이 이해해야만 하는 사정이 생겨 꽤 제대로 공부하게 되었고, 이를 기사로 남겨두고자 합니다.
이 기사에서는 RAG의 흐름을 처음부터 끝까지 따라가며, 각 단계에서 어떤 일이 일어나고 있는지 가능한 한 근본부터 설명하겠습니다. 각 단계에 플로우 차트(Flowchart)를 넣어두었으니, 현재 어느 단계에 있는지를 기준으로 삼아 읽어보시기 바랍니다.
- RAG란 무엇인가
- RAG의 전체상
- 데이터 소스 결정하기
- 크롤링 (Crawling)과 로드 (Load)
- 전처리 (Preprocessing)와 청킹 (Chunking)
- 벡터화 (Vectorization)
- 벡터 DB와 벡터 스토어 (Vector Store)
- 유사도 계산 (Similarity Calculation)
- 벡터 검색 (Vector Search)과 인덱스 (Index)
- 하이브리드 검색 (Hybrid Search)과 재순위화 (Re-ranking)
- 프롬프트 (Prompt) 작성과 답변 생성
- RAG의 평가
- 보안과 운용
- 작은 RAG의 검색 부분을 Python으로 구동하기
RAG는 LLM에 외부 지식을 검색하게 하고, 그 검색 결과를 재료로 삼아 답변하게 하는 메커니즘입니다.
일반적인 LLM은 학습 시점까지 배운 지식과 프롬프트로 전달된 정보를 바탕으로 답변합니다. 하지만 사내 규정, 최신 장애 대응 절차, 제품 사양서, 고객별 계약 조건과 같은 정보는 모델의 학습 데이터에 포함되어 있지 않은 경우가 있습니다. 포함되어 있다 하더라도 오래되었을 가능성이 있습니다.
RAG에서는 답변하기 전에 외부 문서군에서 관련 정보를 찾아, 그 문서를 컨텍스트 (Context)로서 LLM에 전달합니다.
RAG의 흐름은 대략 다음과 같습니다.
- 질문을 받음
- 질문과 관련이 있을 법한 문서를 검색함
- 찾은 문서를 LLM으로 전달함
- LLM이 문서를 근거로 답변함
RAG는 모델 자체에 지식을 써넣는 메커니즘이 아닙니다. 파인튜닝 (Fine-tuning)처럼 모델의 가중치 (Weight)를 바꾸는 것이 아니라, 답변할 때마다 외부 지식을 가져옵니다.
파라미터 (Parameter)에 들어간 지식에만 의존하는 모델에는 지식의 갱신이나 근거 제시라는 과제가 있습니다. RAG는 사전 학습된 모델의 파라미터 지식과 외부의 비 파라메트릭 (Non-parametric) 메모리, 즉 검색 가능한 문서 인덱스를 결합하는 발상입니다12.
RAG는 LLM에게 "전부 외우게 하는" 것이 아니라, 필요할 때 "필요한 자료를 펼치게 하는" 메커니즘이라고 이해하는 것이 가깝습니다.
조금 더 나누면, RAG의 전반부는 검색 시스템, 후반부는 생성형 AI입니다. 검색이 어긋나면 LLM에 전달되는 재료도 어긋납니다. LLM이 아무리 고성능이라도 잘못된 자료만 전달받는다면 올바른 답변을 내놓기 어렵습니다. 즉, RAG에서는 생성뿐만 아니라 검색까지 포함하여 품질을 보게 됩니다!
RAG는 크게 나누면, 사전에 지식 베이스를 만드는 공정과 질문 시에 검색하여 답변하는 공정으로 나뉩니다.
이 그림의 상단 부분이 사전 준비입니다. 문서를 모으고, 불필요한 부분을 제거하고, 검색에서 다룰 단위로 분할하여 벡터로 변환해 저장합니다.
하단 부분이 사용자로부터 질문이 왔을 때의 처리입니다. 질문도 벡터로 변환하여 가까운 문서를 찾고, 필요하다면 순서를 재정렬하여 LLM에 전달해 답변을 만듭니다.
RAG에서 자주 발생하는 실패는 마지막의 LLM만을 보는 것입니다. 답변이 나쁘면 무심코 "모델이 나쁘다"라고 생각하게 되지만, 실제로는 검색 대상 문서가 오래되었거나, 청크 (Chunk)를 나누는 방식이 나쁘거나, 검색 결과가 어긋나거나, 프롬프트에 불필요한 정보를 너무 많이 넣는 등 전 단계의 문제도 있습니다.
RAG는 LLM 기능이라기보다, 검색 시스템과 LLM을 결합한 정보 처리 파이프라인 (Pipeline)입니다12.
이 장의 그림은 RAG에서 자주 사용되는 "비구조화 문서를 청킹하고 벡터 검색하는" 구성을 대표 사례로 보여주고 있습니다. RAG의 본질은 답변 시에 외부 정보를 취득하여 LLM의 컨텍스트에 더하는 것입니다.
RAG는 벡터 DB와 동의어가 아닙니다. 질문이나 데이터의 형태에 따라 적합한 취득 방법은 달라집니다.
| 질문 | 적합한 취득 방법 |
|---|---|
| 교통비 신청 기한은 언제인가요 | 문서의 벡터 검색 (Vector Search) 또는 하이브리드 검색 (Hybrid Search) |
| ... |
규정이나 매뉴얼과 같은 문서에서는 질문과 본문에서 사용되는 단어가 다를 수 있기 때문에, 의미의 유사성을 다룰 수 있는 벡터 검색 (Vector Search)이 유용합니다. 반면, 신청 번호, 재고 수량, 금액, 현재 상태와 같이 정확한 값이 필요한 것은 원래의 데이터베이스나 업무 API로부터 직접 취득하는 것이 더 안전합니다.
넓은 의미에서는 취득 방법이 다르더라도, 취득한 정보를 LLM이 읽을 수 있는 형태로 정돈하여 컨텍스트 (Context)로서 답변 생성에 이용하는 구성을 RAG로 취급하기도 합니다. 다만, API를 통한 업데이트나 외부 시스템 조작은 RAG라기보다는 도구 실행 (Tool Execution)이나 에이전트 (Agent)의 영역으로 구분됩니다.
이후에는 문서 RAG의 대표적인 예로서 벡터 검색 (Vector Search)을 중심으로 다루겠습니다. 특히 이미지를 떠올리기 어렵기도 하니까요.
RAG의 품질은 처음에 입력하는 데이터에 크게 좌우됩니다. 좋은 검색기나 좋은 LLM을 사용하더라도, 원래의 문서가 오래되었거나, 중복이 많거나, 권한이 섞여 있으면 답변의 품질은 올라가지 않습니다.
데이터 소스의 대표적인 예를 들겠습니다.
| 종류 | 예 | 주의점 |
|---|---|---|
| 웹 페이지 (Web Page) | 사내 Wiki, 제품 문서, 공개 FAQ | HTML 본문 추출, 업데이트 감지, robots.txt |
| ... |
주의해야 할 점은 검색 대상을 늘리면 늘릴수록 똑똑해지는 것은 아니라는 점입니다. 불필요한 문서가 늘어나면 검색 시 노이즈 (Noise)도 늘어납니다.
예를 들어, 사내 규정을 답하는 RAG에 오래된 의사록이나 잡담 로그까지 섞으면 검색 결과가 어긋납니다. 최신 공식 문서를 우선시하고, 오래된 문서에는 업데이트 날짜나 버전 번호를 메타데이터 (Metadata)로 갖춰둡니다.
RAG의 데이터 설계에서는 최소한 이러한 메타데이터를 갖춰두면 운용 시에 헤매지 않습니다.
| 메타데이터 | 용도 |
|---|---|
source_url | 답변의 근거 링크로 사용 |
title | 검색 결과나 인용 표시로 사용 |
updated_at | 오래된 문서를 뒤로 밀거나, 재크롤링 (Re-crawling) 판단에 사용 |
document_id | 차분 업데이트 (Incremental Update)나 삭제에 사용 |
chunk_id | 원문 문서 내의 위치를 추적 |
permission_group | 사용자 권한에 따라 검색 결과를 필터링 |
content_hash | 중복 감지나 변경 감지에 사용 |
RAG에서는 "문서를 넣으면 끝"이 아니라, 어디서 온 문서인지, 언제의 정보인지, 누구에게 보여줘도 되는지, 어느 범위를 인용했는지를 추적할 수 있는 상태로 만들어 두어야 합니다.
크롤링 (Crawling)은 웹 페이지 등을 순회하며 문서를 취득하는 공정입니다. RAG에서는 사내 Wiki나 제품 문서를 정기적으로 수집하는 상황에서 자주 등장합니다. "크롤링 성능 개선"이라는 말은 개인적으로 자주 듣는 편입니다.
로드 (Load)는 웹 이외의 데이터를 읽어들이는 처리도 포함하는 용어입니다. PDF, Markdown, Word, 데이터베이스, 티켓 관리 도구, 오브젝트 스토리지 (Object Storage) 등으로부터 RAG에서 다룰 수 있는 텍스트와 메타데이터의 형태로 변환하는 것까지가 로드입니다.
다만, 크롤링은 "URL을 전부 따라가서 저장한다"는 것만이 아닙니다. 실제로는 주로 다음과 같은 것들을 고려합니다.
- 진입 URL 및 사이트맵 (Sitemap) 읽기
- robots.txt 및 액세스 제한 확인
- 페이지 취득
- HTML에서 본문 추출
- 링크 추적
- 중복 URL 및 동일 내용 제외
- 업데이트 일시 및 해시 (Hash) 기록
- 삭제된 페이지 감지
robots.txt는 주로 크롤러의 액세스나 크롤링 부하를 관리하기 위한 메커니즘입니다. 반면, robots.txt는 보안 기구가 아닙니다. 비밀 정보를 지키고 싶다면 robots.txt가 아니라 인증이나 액세스 제어로 지켜야 합니다3. 사이트맵은 크롤링 대상 URL이나 업데이트 정보를 전달하는 보조 정보로 사용할 수 있습니다4.
RAG용 크롤러도 마찬가지입니다. RAG가 읽어서는 안 되는 페이지는 크롤링 설정에서 제외할 뿐만 아니라, 원본 데이터 측의 권한, 취득 시의 인증, 검색 시의 필터링을 통해 이중 삼중으로 보호합니다.
크롤링에서 특히 주의해야 할 것은 본문 이외의 노이즈입니다. 웹 페이지에는 네비게이션 (Navigation), 푸터 (Footer), 사이드바 (Sidebar), 관련 기사, 광고, 브레드크럼 (Breadcrumb) 등이 포함됩니다. 이것들을 그대로 RAG에 넣으면 검색 시 관계없는 정보가 걸려듭니다.
예를 들어, 모든 페이지에 "문의사항은 이쪽으로"라는 푸터가 들어있다면, 문의 관련 질문에서 거의 모든 페이지가 비슷한 문서로 보일 수 있습니다.
따라서 RAG의 로드(Load) 처리에서는 본문을 정돈하기 위한 전처리 (Preprocessing) 과정을 거칩니다.
| 처리 | 목적 |
|---|---|
| HTML 태그 제거 | 검색 대상을 본문 중심으로 설정 |
| ... |
이 흐름을 Python에서 자주 사용되는 Requests와 BeautifulSoup의 조합으로 간단하게 작성해 보겠습니다. 사전에 pip install requests beautifulsoup4로 설치해 둡니다. 코드 중의 example.com은 설명을 위한 플레이스홀더(Placeholder)이므로, 실제로 테스트할 때는 크롤링이 허용된 본인의 검증용 URL로 교체해 주세요.
import hashlib
import urllib.robotparser
import requests
...
실제 사내 크롤러에서는 여기에 링크 순회, 취득 간격 제어, 삭제 페이지 감지 등이 추가됩니다. 그럼에도 중심이 되는 것은 "허가를 확인하고, 취득하고, 본문만 남기고, 변경 사항을 추적할 수 있도록 하는" 이 흐름입니다.
크롤링은 눈에 띄지 않지만 RAG의 토대입니다. 여기가 부실하면 후속되는 벡터 검색 (Vector Search)도 생성 (Generation)도 계속해서 부실한 입력을 상대하게 됩니다. 즉, 매우 중요합니다.
문서를 취득했다면 다음은 청킹 (Chunking)입니다. 청크 (Chunk)란 검색 단위로 사용할 작은 문장 덩어리를 말합니다.
청크는 "저장 단위"이기도 하며 "검색 단위"이기도 합니다. 원본 문서가 큰 PDF나 웹 페이지일지라도, 검색 시에는 작은 청크 단위로 후보에 오릅니다. 청크가 부실하면 검색 결과도 그대로 부실해집니다.
예를 들어, 100페이지짜리 PDF를 통째로 하나의 문서로 벡터화하면, 검색에서 걸리더라도 LLM에 전달하기에는 너무 큽니다. 반대로, 문장 하나하나 너무 잘게 자르면 문맥 (Context)이 손실됩니다.
청킹에서는 검색에서 다룰 수 있고 LLM이 읽을 수 있는 크기로 문서를 자릅니다.
원본 문서가 다음과 같은 내용이라고 가정해 봅시다.
# 경비 정산 규칙
교통비는 업무상 필요한 이동에 한하여 신청할 수 있습니다.
영수증이 있는 경우에는 신청 시 첨첨해 주세요.
...
청킹 후에는 예를 들어 다음과 같은 단위로 나뉩니다.
{
"chunk_id": "expense-rule-001",
"title": "경비 정산 규칙",
...
{
"chunk_id": "expense-rule-002",
"title": "신청 기한",
...
여기서 "경비 정산 규칙"과 "신청 기한"을 나누면, 질문에 대해 필요한 부분만 추출할 수 있습니다.
청크 사이즈 (Chunk Size)는 RAG의 품질을 좌우합니다.
너무 작은 청크는 검색에서 잘 찾아내는 반면 문맥이 부족합니다. 너무 큰 청크는 문맥을 유지할 수 있지만, 관계없는 정보도 함께 LLM에 전달합니다.
| 청크 | 장점 | 단점 |
|---|---|---|
| 작음 | 핀포인트로 검색 가능 | 앞뒤 문맥이 결여됨 |
| ... |
실무에서는 제목이나 단락으로 자르고, 필요에 따라 약간의 중첩을 둡니다. 이 중첩을 오버랩 (Overlap)이라고 부릅니다.
예를 들어, 청크 A의 끝부분과 청크 B의 앞부분에 동일한 문장 수를 넣어두면 경계 부근의 정보도 검색에서 찾아낼 수 있습니다. 다만, 중첩을 너무 늘리면 저장량이 늘어나고 검색 결과도 중복됩니다.
청크 분할은 직접 구현할 수도 있지만, 실무에서는 LangChain의 RecursiveCharacterTextSplitter와 같은 라이브러리도 사용됩니다. pip install langchain-text-splitters로 설치할 수 있으며, 단락, 줄바꿈, 마침표 순으로 구분 위치를 찾으면서 지정된 사이즈에 맞게 분할해 줍니다.
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 크롤링으로 취득한 본문을 가정한 샘플입니다.
document = """# 경비 정산 규칙
...
실행하면 제목의 구분은 유지한 채 두 개의 청크로 나뉩니다.
--- chunk 1 (58자) ---
# 경비 정산 규칙
교통비는 업무상 필요한 이동에 한하여 신청할 수 있습니다.
...
이 예시에서는 설명을 위해 chunk_size=60으로 설정했지만, 실제 문서에서는 수백 자 단위부터 시작하여 검색 결과를 보면서 조정합니다. chunk_overlap을 늘리면 앞서 설명한 오버랩이 포함된 상태로 분할됩니다.
청크에는 본문뿐만 아니라 주변 정보도 포함합니다. 제목이나 문서 제목이 없으면 짧은 본문만으로는 의미가 모호해질 수 있습니다.
예를 들어, "다음 달 5영업일까지 신청해 주세요"라는 청크만 있다면 무엇을 신청하라는 이야기인지 알 수 없습니다. 그래서 제목이나 제목(Heading)을 함께 포함시킵니다.
문서 제목: 경비 정산 규칙
제목: 신청 기한
본문: 경비는 발생일의 다음 달 5영업일까지 신청해 주세요.
이렇게 구성해 두면, "교통비 신청 기한은?"이라는 질문에 대해 문맥을 유지한 채 검색할 수 있습니다.
실무에서는 청크에 parent_document_id를 포함시키기도 합니다. 검색은 작은 청크 단위로 수행하고, LLM에 전달할 때는 동일한 부모 문서의 앞뒤 청크나 제목 정보를 포함하는 설계입니다. 이를 사용하면 검색의 정밀도와 답변 시의 문맥량을 양립할 수 있습니다.
자, 드디어 개인적으로 이미지를 떠올리기 어려운 랭킹 상위의 "벡터화 (Vectorization)"입니다.
벡터화는 문장을 수치 배열로 변환하는 공정입니다. RAG에서 자주 말하는 임베딩 (Embedding)이라고도 불립니다. 임베딩이라는 이름은 문장이라는 복잡한 정보를 수치로만 이루어진 공간 속으로 "심는다(Embed)\
하지만 갑자기 1536개나 3072개의 숫자를 보는 것은 매우 어려우므로, 여기서는 3차원의 작은 예시로 생각해보겠습니다.
예를 들어, 다음과 같은 세 개의 문서가 있다고 가정합니다.
| 문서 | 내용 |
|---|---|
| 문서A | 경비 신청 기한은 다음 달 5영업일까지입니다 |
| ... |
이것들이 만약 다음과 같은 3차원 벡터로 변환되었다고 가정해 봅시다.
$$
d_A = [0.10,\ 0.80,\ 0.20]
$$
$$
d_B = [0.90,\ 0.10,\ 0.20]
$$
$$
d_C = [0.20,\ 0.70,\ 0.60]
$$
여기서 첫 번째 숫자는 '보안스러움', 두 번째 숫자는 '경비·신청스러움', 세 번째 숫자는 '첨부·증적스러움'을 나타낸다고 가정해 보겠습니다. 실제 임베딩 (Embedding) 모델에서는 인간이 각 차원에 이름을 붙일 수는 없지만, 문장을 이러한 의미적 위치에 배치하고 있다고 이해하시면 됩니다.
질문도 마찬가지로 벡터화합니다.
질문이 "교통비는 언제까지 신청해야 합니까?"인 경우, 만약 다음과 같은 벡터가 되었다고 가정합니다.
$$
q = [0.20,\ 0.90,\ 0.10]
$$
이 질문 벡터 q와 문서 벡터 d_A, d_B, d_C를 비교하여 가까운 문서를 검색합니다.
문장과 벡터의 관계를 도식화하면 다음과 같습니다.
문서와 질문 모두 동일한 임베딩 모델을 거쳐 동일한 지도 위에 놓입니다. 질문 q 근처에는 경비·신청에 관한 이야기를 하는 문서A와 문서C가 모이고, 보안 이야기를 하는 문서B는 먼 곳에 배치됩니다. 그 후에는 "지도 위에서 가까운 문서를 찾기"만 하면 의미가 가까운 문서를 찾을 수 있습니다. 이것이 바로 다음 섹션에서 다룰 유사도 (Similarity) 계산의 정체입니다.
Python으로 작성하면, 여기까지의 벡터는 단순한 숫자 리스트로 다룰 수 있습니다.
# 설명용으로 3차원의 작은 벡터를 수동으로 설정합니다.
# 실제 RAG에서는 임베딩 모델이 이 숫자 배열을 반환합니다.
document_vectors = {
...
벡터화는 여기까지입니다. 문서 간의 가까움은 이후에 거리나 각도를 계산하여 구합니다.
실제 RAG에서는 이 숫자 배열을 임베딩 모델의 API를 통해 생성합니다. OpenAI의 임베딩 API를 사용하는 일반적인 작성 방식입니다. pip install openai로 설치하고, API 키는 환경 변수 OPENAI_API_KEY에 설정해 둡니다.
from openai import OpenAI
client = OpenAI()
texts = [
...
출력 형식은 다음과 같은 이미지입니다. 여기에 적힌 숫자는 표시 형식을 보여주기 위한 예시입니다.
경비 신청 기한은 다음 달... 차원 수=1536, 앞부분 3개 요소=[0.0123, -0.0456, 0.0789]
비밀번호는 12자리... 차원 수=1536, 앞부분 3개 요소=[-0.0234, 0.0567, -0.0012]
교통비 영수증은 신청... 차원 수=1536, 앞부분 3개 요소=[0.0098, -0.0345, 0.0654]
벡터의 내용은 인간이 읽어서 의미를 파악할 수 있는 숫자가 아닙니다. 모델이 바뀌면 값도 바뀝니다. 기억해야 할 점은 이 숫자 뭉치가 문장의 "지도상 주소"가 된다는 사실뿐입니다. 참고로, 로컬 환경에서 완결하고 싶은 경우에는 sentence-transformers와 같이 모델을 다운로드하여 실행하는 라이브러리도 사용됩니다.
질문과 문서는 비교 가능한 동일한 임베딩 공간에 놓아야 합니다. 동일한 모델로 양쪽을 모두 처리하는 방식이 가장 직관적이지만, DPR처럼 질문용과 문서용 인코더 (Encoder)가 나누어져 있더라도 동일한 공간으로 투영하도록 훈련된 모델이라면 비교가 가능합니다. 관련 없는 모델이나 전처리를 섞으면 거리의 의미가 무너집니다.
실제 값을 살펴보니 그리 어렵지 않다는 것을 이해하셨을 것입니다.
문서를 청크 (Chunk)로 나누고, 청크를 벡터로 변환하는 단계까지 왔습니다. 다음에 결정할 것은 그 벡터의 저장 위치입니다. 이 단계에서 등장하는 것이 벡터 DB (Vector DB) 또는 벡터 스토어 (Vector Store)입니다.
일반적인 용도 구분 사례로서, 벡터 스토어는 "벡터를 저장하고 검색할 수 있는 장소"를 폭넓게 지칭하는 용어입니다. 벡터 DB는 그중에서도 인덱스 (Index), 유사도 검색, 메타데이터 필터링, 업데이트, 삭제, 권한, 운영 기능까지 통합적으로 다루는 데이터베이스 제품을 지칭하는 경우가 있습니다. 다만, RAG 관련 대화에서는 거의 같은 의미로 사용됩니다.
RAG의 벡터 스토어 (Vector Store)에는 벡터만 넣는 것이 아닙니다. 실제로는 검색 결과를 LLM에 전달하기 위해 필요한 주변 정보도 함께 담습니다.
| 저장하는 것 | 예 | 필요한 이유 |
|---|---|---|
| ID | chunk-001 | 검색 결과를 고유하게 식별하기 위해 |
| 벡터 (Vector) | [0.12, -0.03, ...] | 질문 벡터와의 유사도를 비교하기 위해 |
| 본문 또는 참조처 | 청크 본문, S3 URL | LLM에 근거로 전달하기 위해 |
| 메타데이터 (Metadata) | title, updated_at, source_url | 출처 표시나 필터링(Filtering)에 사용하기 위해 |
| 권한 정보 | tenant_id, permission_group | 보여줘도 되는 문서만 반환하기 위해 |
우선, 벡터 DB는 "숫자로 변환된 문서를 두고, 유사한 것을 찾는 장소"라고 생각해도 무방합니다. 유사도 계산 방법은 다음 장에서 다룹니다.
여기서 지식 베이스 (Knowledge Base), 벡터 스토어 (Vector Store), 원본 문서 스토리지 (Original Document Storage)의 차이점도 구분해 둡니다. RAG에서의 지식 베이스는 특정 제품명이 아니라 "답변에 사용하게 하고 싶은 지식의 집합"을 가리킵니다. 원본 문서, 청크 (Chunk), 벡터, 메타데이터, 출처, 권한, 업데이트 규칙 등을 포함하여 LLM이 참조할 수 있는 지식 창고로 취급합니다.
세 용어의 관계를 정리합니다.
| 용어 | 역할 | 예 |
|---|---|---|
| 지식 베이스 (Knowledge Base) | 답변에 사용하는 지식의 집합 | 사내 규정집, FAQ, 제품 매뉴얼 |
| ... |
예를 들어 "사내 규정 지식 베이스"를 만드는 경우, 원본 PDF는 Amazon S3에 두고, 청크화한 본문과 벡터는 S3 Vectors나 OpenSearch에 넣으며, 검색 시에는 부서나 직책 권한으로 필터링합니다. 이 전체가 지식 베이스입니다. 벡터 스토어는 그 지식 베이스 안에서 검색을 담당하는 부품에 해당합니다.
Amazon Bedrock Knowledge Bases와 같은 매니지드 (Managed) 기능은 이 지식 베이스의 생성과 이용을 AWS 측에서 대신 수행해 주는 구조이며, 내부적으로는 S3 Vectors, OpenSearch, Aurora, Neptune Analytics, Pinecone, Redis Enterprise Cloud, MongoDB Atlas 등의 벡터 스토어를 선택하여 사용합니다8. 즉, Bedrock Knowledge Bases는 "벡터 스토어의 일종"이 아니라, "벡터 스토어를 사용하여 지식 베이스를 만드는 기능"입니다.
AWS에서 RAG를 구축할 때, Amazon S3와 Amazon S3 Vectors를 혼동하는 경우가 있습니다. 참고로 S3 Vectors는 2025년 12월 2일에 일반 제공 (GA)이 시작되었습니다9.
Amazon S3는 PDF나 HTML, 추출된 텍스트와 같은 원본 데이터나 가공된 데이터를 두는 오브젝트 스토리지 (Object Storage)입니다10. 벡터를 JSON 형태로 둘 수도 있지만, 그것만으로는 "이 질문과 유사한 벡터를 찾는다"는 처리를 할 수 없습니다. 반면 Amazon S3 Vectors는 벡터 전용 버킷에 벡터 인덱스 (Vector Index)를 생성하여, 저장된 벡터에 대해 유사도 쿼리 (Similarity Query)나 메타데이터 필터링을 실행할 수 있는 구조입니다11. 공식 문서에서는 저빈도 쿼리에 적합하다고 설명되어 있으나, 2025년 12월 일반 제공 시점에는 성능이 개선되어 빈번한 쿼리에서도 약 100ms 이하의 레이턴시 (Latency)를 보여줍니다9. Bedrock Knowledge Bases나 OpenSearch Service와도 통합됩니다119. 크게 나누면 원본 문서는 S3, 검색용 벡터는 S3 Vectors입니다.
2026년 6월 시점에서 AWS에서 자주 후보가 되는 서비스들을 표로 정리해 둡니다.
| 서비스 | 위치 설정 | 적합한 상황 |
|---|---|---|
| Amazon S3 | 범용 오브젝트 스토리지 | 원본 문서, 가공된 텍스트, 백업을 저렴하고 길게 보관할 때 |
| ... |
OpenSearch Service는 키워드 검색과 조합한 하이브리드 검색 (Hybrid Search)에 강점이 있고12, Aurora PostgreSQL은 pgvector 확장 기능을 통해 SQL과 벡터 검색을 함께 다룰 수 있습니다13. 근접 탐색 (Nearest Neighbor Search) 알고리즘에 관한 자세한 내용은 벡터 검색과 인덱스 장에서 다룹니다.
AWS 외에도 RAG에서 자주 언급되는 서비스나 OSS (Open Source Software)가 있습니다.
| 서비스 | 특징 | 적합한 상황 |
|---|---|---|
| Pinecone | 매니지드 (Managed) 벡터 데이터베이스 | 운영을 맡기고 프로덕션용 벡터 검색을 구축하고 싶을 때 |
| ... |
Pinecone은 매니지드 (Managed) 환경을 전제로 시작할 때의 후보입니다14. Weaviate, Qdrant, Chroma는 OSS (Open Source Software)로서 직접 구동할 수도 있고, 각사의 매니지드 클라우드를 사용할 수도 있습니다151617181920. Milvus는 OSS로서 이용 가능하며, 매니지드로 사용할 경우에는 Milvus를 기반으로 한 Zilliz Cloud도 후보입니다2122. 기존의 DB 자산에 맞춘다면, pgvector23나 MongoDB Atlas Vector Search24도 현실적입니다.
벡터 DB를 선택할 때는 '벡터 검색이 가능한가'만으로 결정해서는 안 됩니다. RAG에서는 본문, 출처, 메타데이터 (Metadata), 권한, 업데이트, 삭제, 평가 로그까지 함께 고려합니다. 특히 사내 문서 RAG에서는 사용자 권한에 따라 검색 결과를 제한할 수 있는지, 오래된 청크 (Chunk)를 확실히 삭제할 수 있는지, 운영 중인 비용을 예측할 수 있는지 확인해야 합니다. RAG의 메커니즘을 이해한 상태에서도 고려해야 할 사항이 많다는, 꽤나 까다로운 작업입니다.
벡터의 저장소를 파악했다면, 다음은 질문 시 '어느 문서가 질문과 유사한가'를 계산합니다.
대표적인 지표가 코사인 유사도 (Cosine Similarity)입니다. 두 벡터가 같은 방향을 향할수록 값이 커집니다. 값은 -1에서 1 사이의 범위에 있으며, 1에 가까울수록 의미가 유사하다고 판단합니다.
$$
\cos(\theta)=\frac{q \cdot d}{|q||d|}
$$
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기