15차시. 기본 RAG 앱 만들기
4. 실습 ② — indexer.py
출처: 원본 PDF 15차시_기본RAG앱만들기.pdf 8~9쪽 · 쪽 그림 8쪽 9쪽
6·7·10차시 코드를 하나로 합칩니다. 인덱스 준비를 전담합니다.
📝 indexer.py
import os, re
from langchain_community.document_loaders import PyPDFLoader
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
import config
NOISE = ["㈜한국주식회사 대외비"]
def _load():
docs = PyPDFLoader(str(config.DOC_PATH)).load()
for d in docs:
text = d.page_content
for n in NOISE:
text = text.replace(n, "")
d.page_content = re.sub(r"\n{3,}", "\n\n", text).strip()
d.metadata["filename"] = config.DOC_PATH.name
d.metadata["page_no"] = d.metadata.get("page", 0) + 1
return docs
def _split(docs):
sp = RecursiveCharacterTextSplitter(
chunk_size=config.CHUNK_SIZE,
chunk_overlap=config.CHUNK_OVERLAP,
separators=["\n\n", "\n", ". ", " ", ""])
chunks = sp.split_documents(docs)
for i, c in enumerate(chunks):
c.metadata["chunk_id"] = i
return chunks
def get_store(rebuild: bool = False):
# 인덱스가 있으면 로드, 없으면 생성 후 저장
emb = OpenAIEmbeddings(model=config.EMBED_MODEL)
if os.path.exists(config.INDEX_PATH) and not rebuild:
return FAISS.load_local(
config.INDEX_PATH, emb,
allow_dangerous_deserialization=True)
chunks = _split(_load())
store = FAISS.from_documents(chunks, emb)
store.save_local(config.INDEX_PATH)
print(f"✓ 인덱스 생성 완료 (조각 {len(chunks)}개)")
return store💡 함수 이름 앞의 밑줄(_)
_load, _split은 내부용이라는 표시입니다.
밖에서는 get_store() 하나만 쓰면 됩니다.
파이썬이 강제하지는 않지만,
"이건 내부 구현이니 신경 쓰지 마세요"라는
개발자들 사이의 약속입니다.
실습 코드 · rag_app/indexer.py
실습 코드: 실습/src/rag_app/indexer.py · 위의 원본 PDF 15차시 8~9쪽 「4️⃣ 실습 ② — indexer.py」과 이어집니다.
# ==============================================================
# 15차시. PPT 15차시_기본RAG앱만들기. 코드 폴더는 src/rag_app
# PPT: 15차시 문서 질의응답
# 역할: PDF를 잘라 FAISS 저장소를 만들거나, 이미 만든 인덱스를 읽어 Retriever 역할을 준비합니다.
# 읽는 키: 없음 (config 상수)
# 쓰는 키: 반환: FAISS 저장소 객체. 새로 만들 때는 인덱스 파일을 디스크에 저장
# 그래프:
# get_store
# |-- index.faiss 와 index.pkl 이 있고 rebuild가 아님 --> 로드
# +-- 없음 --> PDF 로드 --> 청크 --> FAISS 생성 --> 저장
# --------------------------------------------------------------
# Retriever에 해당하는 준비 단계입니다. 질문 함수는 없고, 저장소 객체를 돌려줍니다.
# 청크마다 metadata filename은 PDF 이름, page_no는 로더의 page에 1을 더한 값입니다.
# 임베딩 객체를 만들 때 API 키가 필요합니다. 이 파일은 함수만 정의하므로 직접 실행해도 호출되지 않습니다.
# faiss 인덱스 폴더는 복사하지 않습니다.
# ==============================================================
# Path는 경로를 / 로 이어 붙이고 파일이 있는지 확인할 수 있는 경로 객체입니다.
from pathlib import Path
# PDF 로더, FAISS 저장소, OpenAI 임베딩, 글자 수 기준 분할기를 가져옵니다.
from langchain_community.document_loaders import PyPDFLoader
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 경로, 청크 크기, 모델 이름은 모두 config.py에서 읽습니다.
import config
# 이 파일이 있는 rag_app 폴더입니다. 상대 경로를 이 폴더 기준으로 바꿀 때 씁니다.
BASE = Path(__file__).resolve().parent
# rebuild=True를 주면 저장된 인덱스가 있어도 새로 만듭니다.
def get_store(rebuild=False):
# config의 INDEX_PATH가 상대 경로면 rag_app 폴더 기준 경로로 바꿉니다. 실행 위치가 달라도 같은 곳을 봅니다.
index_path = Path(config.INDEX_PATH)
if not index_path.is_absolute():
index_path = BASE / index_path
# 질문과 조각을 벡터로 바꿀 임베딩 객체입니다. 인덱스를 만들 때와 같은 모델이어야 합니다.
embeddings = OpenAIEmbeddings(model=config.EMBED_MODEL)
# 인덱스 파일 두 개가 모두 있으면 다시 만들지 않고 읽기만 합니다. 질문마다 인덱싱하지 않게 하는 갈림길입니다.
if (index_path / "index.faiss").is_file() and (index_path / "index.pkl").is_file() and not rebuild:
return FAISS.load_local(
str(index_path),
embeddings,
allow_dangerous_deserialization=True,
)
# 인덱스가 없을 때만 여기로 옵니다. PDF를 쪽 단위 Document 목록으로 읽습니다.
documents = PyPDFLoader(str(config.DOC_PATH)).load()
# config의 청크 크기와 겹침 글자 수로 잘게 나눕니다.
splitter = RecursiveCharacterTextSplitter(
chunk_size=config.CHUNK_SIZE,
chunk_overlap=config.CHUNK_OVERLAP,
)
chunks = splitter.split_documents(documents)
# 조각마다 파일 이름과 1부터 시작하는 쪽 번호를 metadata에 붙입니다. 출처 표시에 씁니다.
for document in chunks:
document.metadata["filename"] = Path(config.DOC_PATH).name
document.metadata["page_no"] = document.metadata.get("page", 0) + 1
# 조각을 임베딩해 FAISS 저장소를 만들고 폴더에 저장합니다. 다음 실행부터는 위의 로드 쪽으로 갑니다.
store = FAISS.from_documents(chunks, embeddings)
store.save_local(str(index_path))
return store실행 결과 샘플 (교안 기록. API 키가 필요한 코드는 키 없이 맞춘 예시이고, 모델의 답은 실행할 때마다 달라집니다)
===== 실행 결과 샘플 =====
직접 실행 시 출력 없음
직접 실행 시 출력 없음.
get_store()를 부르면 API 키와 PDF가 필요하므로 호출하지 않았습니다.코드 해설
PDF를 잘라 FAISS 저장소를 만들거나, 이미 만든 인덱스를 읽어 Retriever 역할을 준비합니다.
코드 앞에서 고정해 두는 말입니다.
- 읽는 키: 없음 (config 상수)
- 쓰는 키: 반환: FAISS 저장소 객체. 새로 만들 때는 인덱스 파일을 디스크에 저장
- 그래프:
- get_store
- |-- index.faiss 와 index.pkl 이 있고 rebuild가 아님 --> 로드
- +-- 없음 --> PDF 로드 --> 청크 --> FAISS 생성 --> 저장
- Retriever에 해당하는 준비 단계입니다. 질문 함수는 없고, 저장소 객체를 돌려줍니다.
- 청크마다 metadata filename은 PDF 이름, page_no는 로더의 page에 1을 더한 값입니다.
함수와 클래스로 나누어 읽습니다.
get_store: 이름 그대로 한 가지 일만 합니다. 본문 주석을 같이 읽습니다.
파일 맨 아래 실행 결과 샘플 주석은 실행되지 않습니다. 화면 출력은 다음 절에 다시 적어 두었습니다.