# -*- coding: utf-8 -*- """ build_rag.py 기능: - chunk_and_summary.py 에서 생성된 output/rag/*_chunks.json 파일들을 읽어서 text + summary 를 임베딩(text-embedding-3-small)한다. - FAISS IndexFlatIP 인덱스를 구축하여 output/rag/faiss.index, meta.json, vectors.npy 를 생성한다. """ import os import sys import json from pathlib import Path import numpy as np import faiss from openai import OpenAI from api_config import API_KEYS # ===== 경로 설정 ===== DATA_ROOT = Path(r"D:\for python\survey_test\process") OUTPUT_ROOT = Path(r"D:\for python\survey_test\output") RAG_DIR = OUTPUT_ROOT / "rag" LOG_DIR = OUTPUT_ROOT / "logs" for d in [RAG_DIR, LOG_DIR]: d.mkdir(parents=True, exist_ok=True) # ===== OpenAI 설정 (구조 유지) ===== OPENAI_API_KEY = API_KEYS.get('GPT_API_KEY', '') GPT_MODEL = "gpt-5-2025-08-07" EMBED_MODEL = "text-embedding-3-small" client = OpenAI(api_key=OPENAI_API_KEY) def log(msg: str): print(msg, flush=True) with (LOG_DIR / "build_rag_log.txt").open("a", encoding="utf-8") as f: f.write(msg + "\n") def embed_texts(texts): if not texts: return np.zeros((0, 1536), dtype="float32") embs = [] B = 96 for i in range(0, len(texts), B): batch = texts[i:i+B] resp = client.embeddings.create(model=EMBED_MODEL, input=batch) for d in resp.data: embs.append(np.array(d.embedding, dtype="float32")) return np.vstack(embs) def _build_embed_input(u: dict) -> str: """ text + summary 를 합쳐 임베딩 입력을 만든다. - text, summary 중 없는 것은 생략 - 공백 정리 - 최대 길이 제한 """ sum_ = (u.get("summary") or "").strip() txt = (u.get("text") or "").strip() if txt and sum_: merged = txt + "\n\n요약: " + sum_[:1000] else: merged = txt or sum_ merged = " ".join(merged.split()) if not merged: return "" if len(merged) > 4000: merged = merged[:4000] return merged def build_faiss_index(): docs = [] metas = [] rag_files = list(RAG_DIR.glob("*_chunks.json")) if not rag_files: log("RAG 파일(*_chunks.json)이 없습니다. 먼저 chunk_and_summary.py를 실행해야 합니다.") sys.exit(1) for f in rag_files: try: units = json.loads(f.read_text(encoding="utf-8", errors="ignore")) except Exception as e: log(f"[WARN] RAG 파일 읽기 실패: {f.name} | {e}") continue for u in units: embed_input = _build_embed_input(u) if not embed_input: continue if len(embed_input) < 40: continue docs.append(embed_input) metas.append({ "source": u.get("source", ""), "chunk": int(u.get("chunk", 0)), "folder_context": u.get("folder_context", "") }) if not docs: log("임베딩할 텍스트가 없습니다.") sys.exit(1) log(f"임베딩 대상 텍스트 수: {len(docs)}") E = embed_texts(docs) if E.shape[0] != len(docs): log(f"[WARN] 임베딩 수 불일치: E={E.shape[0]}, docs={len(docs)}") faiss.normalize_L2(E) index = faiss.IndexFlatIP(E.shape[1]) index.add(E) np.save(str(RAG_DIR / "vectors.npy"), E) (RAG_DIR / "meta.json").write_text( json.dumps(metas, ensure_ascii=False, indent=2), encoding="utf-8" ) faiss.write_index(index, str(RAG_DIR / "faiss.index")) log(f"FAISS 인덱스 구축 완료: 벡터 수={len(metas)}") def main(): log("=== FAISS RAG 인덱스 구축 시작 ===") build_faiss_index() log("=== FAISS RAG 인덱스 구축 종료 ===") if __name__ == "__main__": main()