Skip to main content

인덱싱 (Indexing)

인덱싱은 RAG 파이프라인의 첫 번째 단계로, 원본 문서를 검색 가능한 형태로 변환하는 과정입니다. 질문이 들어오기 전에 오프라인으로 미리 수행하며, 인덱싱 품질이 전체 RAG 성능을 좌우합니다.

인덱싱 파이프라인

1. 문서 로딩 (Document Loading)

다양한 형식의 원본 문서를 텍스트로 변환합니다.
문서 형식LangChain Loader특징
PDFPyPDFLoader페이지 단위 분할, 메타데이터 자동 추출
HTMLBSHTMLLoaderBeautifulSoup 기반 파싱
MarkdownUnstructuredMarkdownLoader헤딩 기반 구조 유지
Word (docx)Docx2txtLoader텍스트 추출
CSVCSVLoader행 단위 문서 생성
JSONJSONLoaderjq 스타일 경로 지정
웹 페이지WebBaseLoaderURL에서 직접 로딩

고급 문서 파싱 도구

복잡한 레이아웃(표, 그래프, 수식 등)의 문서를 정확하게 파싱하려면 전용 도구를 사용합니다.
Docling은 IBM Research에서 개발한 문서 변환 도구입니다. PDF, DOCX, PPTX, HTML 등을 Markdown/JSON으로 변환하며, 표 구조 인식, OCR, 수식 변환을 지원합니다.
from docling.document_converter import DocumentConverter

converter = DocumentConverter()
result = converter.convert("document.pdf")

# Markdown으로 변환
markdown = result.document.export_to_markdown()

# LangChain 통합
from docling.chunking import HybridChunker
from langchain_core.documents import Document

chunker = HybridChunker()
chunks = list(chunker.chunk(result.document))
docs = [Document(page_content=chunk.text, metadata=chunk.meta.export_dict()) for chunk in chunks]
특징설명
표 인식TableFormer 모델로 복잡한 표 구조 보존
OCR스캔된 문서, 이미지 내 텍스트 추출
수식LaTeX 변환 지원
레이아웃다단 레이아웃, 헤더/푸터 자동 분리
일반적인 텍스트 중심 PDF에는 PyPDFLoaderPyMuPDF4LLM으로 충분합니다. 복잡한 표, 수식, 다단 레이아웃이 포함된 문서에는 Docling이 더 정확한 결과를 제공합니다.
from langchain_community.document_loaders import PyPDFLoader, WebBaseLoader

# PDF 로딩
loader = PyPDFLoader("document.pdf")
docs = loader.load()

# 웹 페이지 로딩
loader = WebBaseLoader("https://example.com/article")
docs = loader.load()

print(f"로딩된 문서 수: {len(docs)}")
print(f"첫 문서 미리보기: {docs[0].page_content[:200]}")
print(f"메타데이터: {docs[0].metadata}")

2. 텍스트 전처리

로딩된 문서에서 노이즈를 제거하고 메타데이터를 정리합니다.
import re

def clean_text(text: str) -> str:
    """텍스트 전처리: 노이즈 제거 및 정규화"""
    # 연속 공백/줄바꿈 정리
    text = re.sub(r'\n{3,}', '\n\n', text)
    text = re.sub(r' {2,}', ' ', text)
    # 특수 문자 정리
    text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f]', '', text)
    return text.strip()

def enrich_metadata(doc, source_type: str = "pdf"):
    """메타데이터 보강"""
    doc.metadata["source_type"] = source_type
    doc.metadata["char_count"] = len(doc.page_content)
    doc.metadata["word_count"] = len(doc.page_content.split())
    return doc

3. 청킹 (Chunking)

긴 문서를 검색에 적합한 크기의 청크로 분할합니다. 청킹 전략에 따라 검색 품질이 크게 달라집니다.
from langchain_text_splitters import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,      # 청크 최대 크기 (문자 수)
    chunk_overlap=200,    # 청크 간 오버랩
    separators=["\n\n", "\n", ". ", " ", ""],
)

chunks = text_splitter.split_documents(docs)
print(f"청크 수: {len(chunks)}")
청킹 전략의 상세 비교 (Fixed-size, Recursive, Semantic 등)는 청킹 전략 페이지에서 다룹니다.

4. 임베딩 (Embedding)

텍스트 청크를 고차원 벡터로 변환합니다. 동일한 의미의 텍스트는 벡터 공간에서 가까운 위치에 매핑됩니다.

주요 임베딩 모델

모델차원특징비용
OpenAI text-embedding-3-small1536범용, 빠름, 가성비유료 (API)
OpenAI text-embedding-3-large3072고성능, 차원 축소 가능유료 (API)
Cohere embed-v4.01024다국어 지원, 검색 특화유료 (API)
BAAI/bge-m31024오픈소스, 다국어, Dense+Sparse무료
intfloat/multilingual-e5-large1024오픈소스, 다국어무료
from langchain_openai import OpenAIEmbeddings

# OpenAI 임베딩
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

# 단일 텍스트 임베딩
vector = embeddings.embed_query("RAG란 무엇인가요?")
print(f"벡터 차원: {len(vector)}")  # 1536
from langchain_huggingface import HuggingFaceEmbeddings

# 오픈소스 임베딩 (로컬 실행)
embeddings = HuggingFaceEmbeddings(
    model_name="BAAI/bge-m3",
    model_kwargs={"device": "cuda"},
    encode_kwargs={"normalize_embeddings": True},
)

5. 벡터 데이터베이스 저장

임베딩된 벡터를 검색 가능한 데이터베이스에 저장합니다.

주요 벡터 DB 비교

벡터 DB유형특징적합한 환경
Chroma임베디드간단한 설정, 로컬 개발프로토타이핑
Qdrant서버고성능 필터링, 다양한 인덱스프로덕션
Milvus서버대규모 확장, GPU 지원대규모 프로덕션
Weaviate서버하이브리드 검색 내장프로덕션
Pinecone클라우드완전 관리형, 서버리스관리형 서비스
FAISS라이브러리인메모리, 초고속연구/실험
LangChain v1부터 주요 벡터 DB는 전용 패키지로 분리되었습니다.
pip install langchain-chroma    # Chroma
pip install langchain-qdrant    # Qdrant
pip install langchain-pinecone  # Pinecone
pip install langchain-milvus    # Milvus
pip install langchain-weaviate  # Weaviate
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

# 벡터 DB 생성 및 문서 저장
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    collection_name="my_documents",
    persist_directory="./chroma_db",  # 디스크에 영속화
)

# Retriever로 변환
retriever = vectorstore.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 4},
)

전체 인덱싱 파이프라인

from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma

# 1. 문서 로딩
loader = PyPDFLoader("document.pdf")
docs = loader.load()

# 2. 청킹
splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
)
chunks = splitter.split_documents(docs)

# 3. 임베딩 + 벡터 DB 저장
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db",
)

print(f"원본 문서: {len(docs)}개 → 청크: {len(chunks)}개 → 벡터 DB 저장 완료")

인덱싱 품질 체크리스트

너무 작으면 문맥이 손실되고, 너무 크면 노이즈가 포함됩니다. 일반적으로 500~1500자가 적절하며, 도메인에 따라 조정이 필요합니다.
출처, 페이지 번호, 섹션 제목 등의 메타데이터를 저장하면 검색 시 필터링과 출처 인용에 활용할 수 있습니다.
범용 임베딩 모델이 특정 도메인(의료, 법률 등)에서 성능이 낮을 수 있습니다. 파인튜닝이나 도메인 특화 모델을 고려하세요.
동일한 내용이 여러 청크에 반복되면 검색 결과의 다양성이 떨어집니다. 중복 제거(deduplication)를 적용하세요.