AI Engineering Academy · 강의

색인: 분할 조각 임베딩 및 저장

OpenAI 임베딩 API로 각 분할 조각을 임베딩하고, 결과 벡터를 메타데이터와 함께 벡터 저장소에 업서트하여 문서를 검색 가능한 색인으로 구축합니다.

레슨 3/413개 단계

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

색인 단계: 개요

문서를 로드하고 청킹한 후에는 색인 단계에 도달합니다. 이 단계에서는 텍스트 청크를 벡터 임베딩으로 변환하여 검색 가능한 벡터 데이터베이스에 저장합니다. 질의에 답변하기 전에 수행하는 마지막 오프라인 단계입니다. 임베딩 품질과 저장 및 색인 전략의 효율성이 질의 시 RAG 시스템의 속도와 정확도를 직접 결정합니다.

OpenAI API를 통한 임베딩 생성

가장 일반적인 방법은 청크 텍스트와 함께 OpenAI의 임베딩 API를 호출하는 것입니다. text-embedding-3-small 모델은 1,536차원 벡터를 생성하며 100만 토큰당 0.02달러의 비용이 들어 대부분의 작업에 매우 저렴합니다. 처리량을 극대화하려면 한 번의 API 호출로 여러 텍스트를 보내세요(최대 2,048개 입력). 응답에는 입력 텍스트와 동일한 순서로 각 입력에 해당하는 임베딩 벡터가 하나씩 포함됩니다.

from openai import OpenAI

client = OpenAI()

def embed_batch(texts, model='text-embedding-3-small'):
    response = client.embeddings.create(
        model=model,
        input=texts  # up to 2048 texts per call
    )
    return [item.embedding for item in response.data]

# Embed one batch of 100 chunk texts
texts = [chunk['text'] for chunk in chunks[:100]]
vectors = embed_batch(texts)
print(f'Embedding dimension: {len(vectors[0])}')
print(f'Embedded {len(vectors)} chunks')

효율성을 위한 일괄 처리

수천 개의 청크를 색인할 때는 효율성이 중요합니다. 처리량과 메모리 사용량의 균형을 맞추도록 청크를 100~500개 단위로 처리하세요. 실패 후 이미 처리한 청크를 다시 임베딩하지 않고 재개할 수 있도록 현재 위치를 추적하세요. 진행 상황도 정기적으로 기록하세요. 청크 100,000개를 500개씩 처리하면 API를 200번 호출하게 되며, 일반적으로 몇 분 안에 완료됩니다.

def embed_all_chunks(chunks, batch_size=200):
    embedded = []
    total = len(chunks)
    for i in range(0, total, batch_size):
        batch = chunks[i:i+batch_size]
        texts = [c['text'] for c in batch]
        vectors = embed_batch(texts)
        for chunk, vector in zip(batch, vectors):
            embedded.append({
                **chunk,
                'embedding': vector
            })
        if (i // batch_size) % 10 == 0:
            print(f'Progress: {min(i+batch_size, total)}/{total}')
    return embedded

색인 중 속도 제한 처리

OpenAI 임베딩 API에는 분당 토큰 수(TPM)로 측정되는 속도 제한이 있습니다. 대규모 색인 작업은 이러한 제한에 도달하여 RateLimitError를 받게 됩니다. 지터를 적용한 지수 백오프를 구현하세요. 속도 제한 오류가 발생하면 재시도하기 전에 짧고 무작위적인 시간 동안 기다리고, 이후 실패할 때마다 대기 시간을 두 배로 늘립니다. 이렇게 하면 재시도가 분산되어 모든 병렬 작업자가 동시에 API를 몰아붙이는 일을 방지할 수 있습니다.

import time
import random
from openai import RateLimitError

def embed_batch_with_retry(texts, max_retries=5):
    for attempt in range(max_retries):
        try:
            return embed_batch(texts)
        except RateLimitError:
            if attempt == max_retries - 1:
                raise
            wait = (2 ** attempt) + random.uniform(0, 1)
            print(f'Rate limited. Waiting {wait:.1f}s...')
            time.sleep(wait)
    return []

Pinecone에 벡터 업서트

임베딩을 생성한 후 벡터 저장소에 upsert합니다. Upserting은 새 벡터를 삽입하거나 동일한 ID가 이미 있으면 기존 벡터를 업데이트하는 작업이며, 처음부터 멱등성을 갖도록 설계됩니다. Pinecone에서는 upsert된 각 레코드에 벡터 ID, 임베딩 값, 그리고 나중에 필터링하거나 표시할 필드가 담긴 메타데이터 사전이 포함됩니다. 처리량을 최적화하려면 호출당 최대 100개의 레코드씩 묶어 upsert하세요.

import pinecone

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

def upsert_to_pinecone(embedded_chunks, batch_size=100):
    for i in range(0, len(embedded_chunks), batch_size):
        batch = embedded_chunks[i:i+batch_size]
        vectors = [
            (
                chunk['id'],
                chunk['embedding'],
                {
                    'text': chunk['text'],
                    'source': chunk['metadata']['source'],
                    'page': chunk['metadata'].get('page', 0)
                }
            )
            for chunk in batch
        ]
        index.upsert(vectors=vectors)
        print(f'Upserted {min(i+batch_size, len(embedded_chunks))}/{len(embedded_chunks)}')

pgvector에 저장하기

pgvector를 사용하면 표준 SQL로 PostgreSQL 테이블에 임베딩을 직접 삽입할 수 있습니다. vector 데이터 형식은 문자열로 직렬화된 실수의 Python 목록을 허용합니다. 모든 행을 삽입한 후 빠른 근사 최근접 이웃 쿼리를 위해 HNSW Index를 생성하세요. 수백만 개의 행이 있는 기존 테이블에 Index를 생성하는 데는 몇 분이 걸릴 수 있으므로, 대량 삽입 전에 생성하지 말고 삽입이 끝난 후 Index를 구축하세요.

import psycopg2
from psycopg2.extras import execute_values

def upsert_to_pgvector(conn, embedded_chunks):
    with conn.cursor() as cur:
        records = [
            (
                chunk['id'],
                chunk['text'],
                chunk['metadata']['source'],
                chunk['metadata'].get('page', 0),
                chunk['embedding']   # list of floats
            )
            for chunk in embedded_chunks
        ]
        execute_values(cur, '''
            INSERT INTO document_chunks (id, text, source, page, embedding)
            VALUES %s
            ON CONFLICT (id) DO UPDATE
            SET text = EXCLUDED.text, embedding = EXCLUDED.embedding
        ''', records)
    conn.commit()

HNSW Index 구축

HNSW (Hierarchical Navigable Small World)는 빠른 근사 최근접 이웃 검색을 가능하게 하는 Index 유형입니다. 모든 저장된 벡터와 쿼리 벡터를 비교하는 완전 탐색과 달리, HNSW는 검색 공간을 줄이는 다층 그래프 구조를 구축합니다. m 매개변수는 각 노드의 연결 수를 제어합니다(값이 높을수록 재현율은 좋아지지만 메모리를 더 많이 사용함). ef_construction은 구축 중 Index 품질을 제어합니다.

-- Build HNSW index after bulk insertion
CREATE INDEX CONCURRENTLY ON document_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

-- Set ef_search at query time to trade recall vs speed
SET hnsw.ef_search = 100;

-- Verify index was created
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'document_chunks';

메타데이터 스키마 설계

각 벡터와 함께 저장되는 메타데이터를 활용하면 강력한 필터링 검색이 가능합니다. Index를 생성하기 전에 메타데이터 스키마를 설계하세요. 나중에 새 필드를 추가하려면 다시 Index를 생성해야 합니다. 필터링에 사용할 필드(부서, 문서 유형, 날짜 범위), 인용에 표시할 필드(제목, 페이지, 작성자), 디버깅에 유용한 필드(chunk_index, total_chunks, indexed_at)를 포함하세요. 메타데이터 값은 단순하게 유지하세요. 문자열, 숫자, 불리언은 효율적으로 Index되고 필터링되지만 중첩 객체는 그렇지 않습니다.

# Well-designed metadata schema
METADATA_SCHEMA = {
    # For filtering at retrieval time
    'department': 'HR',         # string
    'doc_type': 'policy',       # string
    'year': 2025,               # integer
    'is_active': True,          # boolean

    # For display in citations
    'title': 'Employee Handbook 2025',
    'author': 'HR Team',
    'page': 12,
    'source': 's3://docs/handbook_2025.pdf',

    # For debugging and updates
    'chunk_index': 3,
    'total_chunks': 24,
    'indexed_at': '2025-09-01T10:00:00Z'
}

장시간 Index 작업의 체크포인트 저장

대규모 말뭉치를 Index하는 데는 몇 시간이 걸릴 수 있습니다. 중간에 충돌이 발생하면 지금까지의 모든 진행 상황을 잃게 됩니다. 성공적으로 Index된 청크를 기록하는 체크포인트 파일을 구현하세요. 다시 시작할 때는 이미 Index된 청크를 건너뛰고 중단한 지점부터 계속 진행합니다. 이렇게 하면 Index 작업을 멱등적이고 안전하게 재개할 수 있습니다. 체크포인트는 처리된 청크 ID의 집합으로 저장하며, JSON 파일이나 데이터베이스 테이블을 사용하면 됩니다.

import json
from pathlib import Path

CHECKPOINT_FILE = '/tmp/index_checkpoint.json'

def load_checkpoint():
    if Path(CHECKPOINT_FILE).exists():
        return set(json.loads(Path(CHECKPOINT_FILE).read_text()))
    return set()

def save_checkpoint(indexed_ids):
    Path(CHECKPOINT_FILE).write_text(json.dumps(list(indexed_ids)))

def index_with_checkpoint(chunks, index):
    done = load_checkpoint()
    remaining = [c for c in chunks if c['id'] not in done]
    print(f'Resuming: {len(done)} done, {len(remaining)} remaining')
    for chunk in remaining:
        upsert_to_pinecone([chunk], index)
        done.add(chunk['id'])
        save_checkpoint(done)

Index 완전성 확인

Index를 생성한 후 모든 청크가 벡터 저장소에 들어갔는지 확인하세요. 분할기가 생성한 청크 수와 Index가 보고하는 벡터 수를 비교하세요. 알려진 문서의 텍스트로 Index를 쿼리하고 예상한 결과가 상위 5개 안에 나타나는지 확인하세요. 정답이 알려진 테스트 세트에서 몇 가지 쿼리를 실행하고 정밀도가 예상 수준인지 확인하세요. 확인하지 않고 Index가 완전하다고 가정하지 마세요.

def verify_index(index, chunks, sample_size=10):
    index_stats = index.describe_index_stats()
    total_vectors = index_stats.total_vector_count
    expected = len(chunks)
    print(f'Index vectors: {total_vectors}, Expected: {expected}')
    if total_vectors != expected:
        print('WARNING: mismatch — some chunks may not have been indexed')

    # Spot-check retrieval
    import random
    sample = random.sample(chunks, sample_size)
    for chunk in sample:
        vec = embed_batch([chunk['text']])[0]
        results = index.query(vector=vec, top_k=1, include_metadata=True)
        top_id = results.matches[0].id if results.matches else None
        if top_id != chunk['id']:
            print(f'WARNING: expected {chunk["id"]}, got {top_id}')

로컬 임베딩 대안

인프라 밖으로 반출할 수 없는 개인정보 보호가 중요한 데이터에는 로컬 호스팅 임베딩 모델을 사용하세요. sentence-transformers 라이브러리는 all-MiniLM-L6-v2(384차원, 22MB, 매우 빠름)와 bge-large-en-v1.5(1024차원, 더 높은 품질) 같은 고품질 모델을 제공합니다. 중간 규모의 작업에는 CPU로 실행하고 대규모 Index 작업에는 GPU를 사용하세요. 로컬 모델을 사용하면 API 비용과 데이터 반출을 없앨 수 있지만, 모델 파일과 연산 리소스를 직접 관리해야 합니다.

from sentence_transformers import SentenceTransformer

# Load once at startup
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')

def embed_locally(texts, batch_size=64):
    # encode() handles batching internally
    embeddings = model.encode(
        texts,
        batch_size=batch_size,
        show_progress_bar=True,
        convert_to_numpy=True
    )
    return embeddings.tolist()  # convert numpy array to Python list

vectors = embed_locally([c['text'] for c in chunks])

빠른 확인

이 레슨에서 배운 AI 엔지니어링 개념을 제대로 이해했는지 테스트해 보세요.

레슨 요약

이 레슨에서는 OpenAI API로 배치 단위로 임베딩을 생성하고 지수 백오프로 요청 제한을 처리하는 방법, 메타데이터와 함께 Pinecone 및 pgvector에 벡터를 upsert하는 방법, 빠른 근사 최근접 이웃 검색을 위한 HNSW Index 구축, 그리고 체크포인트 기반 재개, 메타데이터 스키마 설계, Index 완전성 확인을 포함한 프로덕션 모범 사례를 배웠습니다. 다음에는 청크를 검색하고 근거가 있는 답변을 생성하는 쿼리 pipeline을 구축합니다.

무료로 시작

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

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

코스
30
레슨
120

자주 묻는 질문

“색인: 분할 조각 임베딩 및 저장” 강의는 무료인가요?

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

“색인: 분할 조각 임베딩 및 저장”에서 뭘 배우나요?

OpenAI 임베딩 API로 각 분할 조각을 임베딩하고, 결과 벡터를 메타데이터와 함께 벡터 저장소에 업서트하여 문서를 검색 가능한 색인으로 구축합니다. 브라우저에서 직접 실행하는 실습 코드로 AI Engineering Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

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

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

“색인: 분할 조각 임베딩 및 저장” 강의는 얼마나 걸리나요?

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

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

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

이 강의의 모든 강의

  1. 문서 불러오기와 텍스트 추출
  2. 분할 전략: 고정 크기, 문장 단위, 재귀 방식
  3. 색인: 분할 조각 임베딩 및 저장
  4. 질의, 검색, 생성
← AI Engineering Academy(으)로 돌아가기