AI Engineering Academy · 강의

RAG 아키텍처: 색인과 검색

RAG의 두 단계를 살펴봅니다. 문서를 분할하고 임베딩해 저장하는 오프라인 색인 단계와, 각 질의에 맞는 관련 컨텍스트를 찾는 온라인 검색 단계입니다.

레슨 2/413개 단계

RAG 아키텍처: 색인과 검색은(는) CoddyKit의 무료 AI Engineering Academy 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 AI Engineering Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. AI Engineering Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

RAG에는 서로 다른 두 단계가 있습니다

RAG 시스템은 서로 근본적으로 다른 두 단계로 작동하며, 각 단계는 서로 다른 시점에 실행됩니다. 오프라인 색인 단계에서는 문서를 한 번(또는 문서가 변경될 때) 처리하여 검색 가능한 색인을 준비합니다. 온라인 검색 단계는 모든 사용자 질의에 대해 실시간으로 실행됩니다. 이러한 분리를 이해하는 것은 질의 시점에는 빠르고 시간이 지나도 유지 관리하기 쉬운 시스템을 설계하는 데 필수적입니다.

색인 단계: 1단계 — 불러오기

색인은 문서 불러오기로 시작합니다. 이는 원본 시스템에서 원시 파일을 읽는 과정입니다. 문서는 PDF, Word 파일, HTML 페이지, Markdown 파일, 데이터베이스 행 또는 모든 텍스트 원본일 수 있습니다. 각 문서는 가능한 경우 구조를 유지한 일반 텍스트로 메모리에 불러옵니다. pypdf, python-docx, unstructured와 같은 라이브러리가 형식별 구문 분석과 같은 복잡한 작업을 처리합니다.

from pypdf import PdfReader

def load_pdf(path):
    reader = PdfReader(path)
    pages = []
    for i, page in enumerate(reader.pages):
        text = page.extract_text()
        pages.append({'text': text, 'page': i + 1, 'source': path})
    return pages

docs = load_pdf('company_policy.pdf')
print(f'Loaded {len(docs)} pages')

색인 단계: 2단계 — 청크로 나누기

LLM의 컨텍스트 창은 유한하며 문서 전체를 검색하는 것은 비효율적입니다. 불러온 텍스트는 각각 대략 200~1000개의 토큰으로 이루어진 작은 청크로 나뉩니다. 좋은 청크 분할은 의미적 일관성을 유지합니다. 즉, 하나의 청크는 완결된 하나의 생각을 표현해야 합니다. 일반적인 전략은 겹치는 창을 사용하여 청크 경계 근처의 문장이 두 청크에 나타나도록 하는 것이며, 이를 통해 분할 지점에서 정보가 손실되는 것을 방지합니다.

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,      # characters per chunk
    chunk_overlap=50,    # overlap between chunks
    separators=['\n\n', '\n', '. ', ' ']
)

for page in docs:
    chunks = splitter.split_text(page['text'])
    for chunk in chunks:
        # Each chunk carries metadata from its source page
        print(f'Chunk ({len(chunk)} chars): {chunk[:80]}...')

색인 단계: 3단계 — 임베딩

각 텍스트 청크는 의미를 수치로 표현하는 밀집 벡터 임베딩으로 변환됩니다. 각 청크에 대해 text-embedding-3-small과 같은 임베딩 모델을 호출하면 고차원 실수 배열을 받습니다. 의미가 비슷한 청크는 이 고차원 공간에서 서로 가까운 벡터가 되며, 이것이 의미 유사도 검색을 가능하게 합니다.

from openai import OpenAI

client = OpenAI()

def embed_chunks(chunks):
    texts = [c['text'] for c in chunks]
    response = client.embeddings.create(
        model='text-embedding-3-small',
        input=texts
    )
    for i, chunk in enumerate(chunks):
        chunk['embedding'] = response.data[i].embedding
    return chunks

# Batch up to 2048 texts per API call
embedded = embed_chunks(all_chunks)

색인 단계: 4단계 — 저장

임베딩된 청크는 메타데이터(원본 파일, 페이지 번호, 섹션 제목)와 함께 벡터 데이터베이스에 저장됩니다. 벡터 저장소는 빠른 근사 최근접 이웃 검색을 가능하게 하는 색인 구조(일반적으로 HNSW)를 구축합니다. 이 색인은 디스크에 저장되므로 시스템을 다시 시작해도 유지됩니다. 색인은 일반적으로 설정 시 한 번 수행하고, 새 문서가 추가될 때 증분 방식으로 수행합니다.

import pinecone

pc = pinecone.Pinecone(api_key='YOUR_KEY')
index = pc.Index('rag-documents')

# Upsert vectors with metadata
vectors_to_upsert = [
    (
        chunk['id'],
        chunk['embedding'],
        {'text': chunk['text'], 'source': chunk['source'], 'page': chunk['page']}
    )
    for chunk in embedded_chunks
]

# Upsert in batches of 100
for i in range(0, len(vectors_to_upsert), 100):
    index.upsert(vectors=vectors_to_upsert[i:i+100])
print('Indexing complete')

검색 단계: 질의 임베딩

사용자가 질의를 제출하면 온라인 검색 단계가 시작됩니다. 첫 단계는 색인에 사용한 것과 동일한 임베딩 모델을 사용하여 사용자의 질문을 임베딩하는 것입니다. 이는 매우 중요합니다. text-embedding-3-small로 색인했다면 질의에도 반드시 text-embedding-3-small을 사용해야 합니다. 질의 임베딩은 사용자가 무엇을 묻는지 그 의미를 인코딩한 벡터입니다.

def embed_query(question):
    response = client.embeddings.create(
        model='text-embedding-3-small',  # MUST match indexing model
        input=[question]
    )
    return response.data[0].embedding

user_question = 'What is our parental leave policy?'
query_vector = embed_query(user_question)
print(f'Query embedded: {len(query_vector)}-dim vector')

검색 단계: ANN 검색

질의 임베딩은 벡터 저장소로 전송되고, 벡터 저장소는 근사 최근접 이웃(ANN) 검색을 수행하여 임베딩이 질의 벡터와 가장 유사한 K개의 청크를 찾습니다. HNSW 색인은 무차별 대입 검색보다 약간의 재현율을 희생하는 대신 속도를 크게 높이므로 이 검색은 매우 빠릅니다(일반적으로 10밀리초 미만). 상위 K개의 청크를 검색하며, K=5에서 K=20을 사용하는 경우가 많습니다.

results = index.query(
    vector=query_vector,
    top_k=5,
    include_metadata=True
)

print(f'Retrieved {len(results.matches)} chunks:')
for match in results.matches:
    print(f'  Score: {match.score:.3f} | Source: {match.metadata["source"]}')
    print(f'  Text: {match.metadata["text"][:100]}...')
    print()

두 단계 연결하기

핵심은 색인과 검색이 함께 작동하도록 설계된다는 점입니다. 벡터가 존재하는 수학적 공간은 모델마다 다르므로 두 단계에서 동일한 임베딩 모델을 사용해야 합니다. 임베딩 모델을 바꾸면 모든 문서를 다시 색인해야 합니다. 벡터 저장소는 두 단계를 연결하는 다리입니다. 색인 중에는 벡터를 받아들이고 검색 중에는 벡터를 반환하여, 벡터 공간에서는 두 단계를 정렬된 상태로 유지하면서 시간상으로는 서로 분리합니다.

검색 중 메타데이터 필터링

벡터 검색은 의미적으로 유사한 청크를 찾지만, 때로는 메타데이터로 필터링해야 합니다. 예를 들어 2025년에 업로드된 문서의 청크만 검색하거나 HR 부서 폴더의 청크만 검색할 수 있습니다. 벡터 저장소는 메타데이터 필드에 대한 사전 필터링 또는 사후 필터링을 지원합니다. Pinecone과 Qdrant가 지원하는 사전 필터링은 ANN 검색 전에 필터를 적용하므로 상위 K개 결과에 사후 필터링을 적용하는 것보다 빠르고 정확합니다.

# Retrieve only from HR department documents
results = index.query(
    vector=query_vector,
    top_k=5,
    filter={'department': {'$eq': 'HR'}},
    include_metadata=True
)

# Or filter by date range
results = index.query(
    vector=query_vector,
    top_k=5,
    filter={
        'upload_year': {'$gte': 2024},
        'doc_type': {'$eq': 'policy'}
    },
    include_metadata=True
)

업데이트를 위한 증분 색인

운영 환경에서는 문서 말뭉치가 시간이 지나며 변경됩니다. 효과적인 색인 아키텍처는 증분 업데이트를 지원합니다. 문서가 편집되면 ID로 기존 벡터를 삭제하고 새 벡터를 업서트합니다. 문서가 삭제되면 해당 벡터를 제거합니다. 각 청크에 원본 문서 경로와 청크 위치를 기반으로 결정론적인 ID를 할당하면 모든 내용을 다시 색인하지 않고도 항상 올바른 벡터를 찾아 업데이트할 수 있습니다.

import hashlib

def make_chunk_id(source_path, chunk_index):
    # Deterministic, stable ID for each chunk
    key = f'{source_path}::chunk_{chunk_index}'
    return hashlib.md5(key.encode()).hexdigest()

def update_document(source_path, index):
    # Delete old vectors for this document
    index.delete(filter={'source': source_path})
    # Re-index the updated document
    new_chunks = load_and_chunk(source_path)
    new_embedded = embed_chunks(new_chunks)
    index.upsert(vectors=new_embedded)
    print(f'Updated {source_path}: {len(new_chunks)} chunks')

전체 RAG 파이프라인 한눈에 보기

전체 RAG 아키텍처는 다음과 같습니다. 오프라인: 문서 → 로더 → 청크 분할기 → 임베딩 모델 → 벡터 저장소. 온라인: 사용자 질의 → 임베딩 모델 → 벡터 저장소(ANN 검색) → 상위 K개 청크 → 프롬프트 조합 → LLM → 답변. 오프라인 파이프라인은 문서가 업데이트될 때마다 문서별로 한 번 실행됩니다. 온라인 파이프라인은 모든 사용자 질의에 대해 밀리초 단위로 실행되며, LLM은 전체 문서 말뭉치가 아니라 관련 컨텍스트만 확인합니다.

빠른 확인

이번 레슨에서 배운 AI 엔지니어링 개념을 이해했는지 확인해 보세요.

레슨 요약

이번 레슨에서는 문서마다 한 번 실행되는 불러오기, 청크 분할, 임베딩, 저장 단계로 구성된 오프라인 색인 단계, 질의를 임베딩하고 ANN 검색을 수행하여 밀리초 단위로 상위 K개 청크를 반환하는 온라인 검색 단계, 그리고 문서가 변경될 때 벡터 저장소를 최신 상태로 유지하는 증분 색인 전략을 배웠습니다. 다음에는 검색된 컨텍스트를 효과적으로 활용하는 증강 프롬프트를 작성하는 방법을 살펴보겠습니다.

무료로 시작

AI 튜터와 함께 Python을(를) 배우세요 — 무료

브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.

코스
30
레슨
120

자주 묻는 질문

“RAG 아키텍처: 색인과 검색” 강의는 무료인가요?

네 — “RAG 아키텍처: 색인과 검색” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 AI Engineering Academy 강의 전체를 잠금 해제할 수 있습니다. AI Engineering Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“RAG 아키텍처: 색인과 검색”에서 뭘 배우나요?

RAG의 두 단계를 살펴봅니다. 문서를 분할하고 임베딩해 저장하는 오프라인 색인 단계와, 각 질의에 맞는 관련 컨텍스트를 찾는 온라인 검색 단계입니다. 브라우저에서 직접 실행하는 실습 코드로 AI Engineering Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

AI Engineering Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 AI Engineering Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“RAG 아키텍처: 색인과 검색” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 AI Engineering Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 AI Engineering Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. RAG가 해결하는 문제
  2. RAG 아키텍처: 색인과 검색
  3. 증강 프롬프트 작성하기
  4. RAG와 미세 조정: 무엇을 언제 사용할까
← AI Engineering Academy(으)로 돌아가기