Spaces:
Sleeping
Sleeping
| """ | |
| src/rag/ingest.py | |
| SAP Learning Hub PDF λ¬Έμ νμ± β μ²νΉ β λ©νλ°μ΄ν° λΆμ°© β ChromaDB μΈμ μ€νΈ | |
| ν΅μ¬ μ€κ³: | |
| 1) μ²ν¬λ§λ€ 컬λ μ μ μμμ μ μΌνκ³ μ¬μΈμ μ€νΈν΄λ μμ μ μΈ chunk_id λΆμ¬ | |
| (νμ: "<source_stem>::NNNN"). μ΄ κ°μ ChromaDB document id λ‘λ λμΌνκ² μ¬μ© | |
| β retrieved μ²ν¬μ metadata["chunk_id"] λ§μΌλ‘ μ λ΅ μ¬λΆλ₯Ό μ ν λ§€μΉ (id κΈ°μ€ RAG νκ°) | |
| 2) λ¬Έμ λ¨μ λ©ν(source, doc_title, total_pages, total_chunks) + | |
| μ²ν¬ λ¨μ λ©ν(chunk_id, chunk_index, page, char_count, unit, lesson, section) λΆμ°© | |
| β μΈμ©Β·νν°λ§Β·νκ°μ μ¬μ©. PDF λ‘λκ° μ±μ°λ μ‘λ€ν νλλ μ κ±°νκ³ μ μ©ν κ²λ§ μ μ§ | |
| 3) SAP Learning Hub PDFλ νμ΄μ§ νΈν°μ 'Unit N: μ λͺ©'(μ§μ λ©΄) / 'Lesson: μ λͺ©'(νμ λ©΄)μ΄ | |
| μΌκ΄λκ² λ°ν μμ. μ΄λ₯Ό νμ΄μ§ λ¨μλ‘ μΆμΆ β forward-fill ν΄μ κ° μ²ν¬μ μ£Όμ (unit/lesson)λ₯Ό | |
| λ©νλ‘ λΆμ°©νκ³ , λ°λ³΅ νΈν°(μ μκΆ/Unit/Lesson μ€)λ λ³Έλ¬Έμμ μ κ±°ν΄ μλ² λ© λ Έμ΄μ¦λ₯Ό μ€μΈλ€. | |
| 4) (μ΅μ ) κ° μ²ν¬ λ³Έλ¬Έ μμ 컨ν μ€νΈ ν€λλ₯Ό prepend ν΄μ λ©ν μ 보λ₯Ό μλ² λ©μ λ°μ | |
| configs.yaml: rag.contextual_header | |
| 5) (μ΅μ ) κ° PDF νμ΄μ§μ λ΄μ₯ μ΄λ―Έμ§λ₯Ό Surya OCRλ‘ μΈμ β λ³λ OCR μ²ν¬λ‘ ChromaDBμ μΆκ° | |
| chunk_id νμ: "<source_stem>_ocr::NNNN" / metadata.source_type = "ocr" | |
| configs.yaml: rag.ocr_enabled | |
| chunk_size / chunk_overlap / collection_name / contextual_header / ocr_enabled λ±μ configs.yamlμμ μ½μ΅λλ€. | |
| """ | |
| import io | |
| import re | |
| from pathlib import Path | |
| import fitz # PyMuPDF β PDF λΆλ§ν¬(outline) μ§μ μ‘°νμ© | |
| from langchain_community.document_loaders import PyMuPDFLoader | |
| from langchain_text_splitters import RecursiveCharacterTextSplitter | |
| from langchain_community.embeddings import HuggingFaceBgeEmbeddings | |
| from langchain_community.vectorstores import Chroma | |
| from langchain_core.documents import Document | |
| from src.config import get_config | |
| DOCS_DIR = "data/docs" | |
| # SAP Learning Hub PDF νΈν° ν¨ν΄. | |
| # μ§μ λ©΄ νΈν°: "Unit 4: Master Data" | |
| # νμ λ©΄ νΈν°: "Lesson: Maintaining Business Partner Master Data" | |
| # κ³΅ν΅ νΈν° : "Β© Copyright. All rights reserved." + νμ΄μ§ λ²νΈ | |
| # μ μκΆ μ€μ μ΅μ»€λ‘ μ‘κ³ κ·Έ μμͺ½μμ Unit/Lesson μ€μ μ°ΎμΌλ©΄ λ³Έλ¬Έ λ΄ λμΌ λ¬Έκ΅¬μ νΌλ μμ΄ | |
| # 'λ¬λ νΈν°'λ§ μ νν μ§λλ€. | |
| _UNIT_FOOTER = re.compile(r"^\s*Unit\s+(\d+):\s+(.+?)\s*$") | |
| _LESSON_FOOTER = re.compile(r"^\s*Lesson:\s+(.+?)\s*$") | |
| _COPYRIGHT = re.compile(r"Copyright", re.I) | |
| def _build_chunk_id(source_stem: str, index: int) -> str: | |
| """컬λ μ μ μμμ μ μΌνκ³ μ¬μΈμ μ€νΈν΄λ μμ μ μΈ μ²ν¬ id.""" | |
| return f"{source_stem}::{index:04d}" | |
| def _extract_footers(page_text: str) -> tuple[str | None, str | None]: | |
| """ν νμ΄μ§μ νΈν°μμ (unit, lesson) μΆμΆ. μμΌλ©΄ (None, None).""" | |
| lines = [l.strip() for l in page_text.splitlines() if l.strip()] | |
| anchor = next((i for i, l in enumerate(lines) if _COPYRIGHT.search(l)), len(lines)) | |
| unit = lesson = None | |
| for l in reversed(lines[:anchor]): # μ μκΆ μ€ λ°λ‘ μ β νΈν° | |
| if unit is None: | |
| m = _UNIT_FOOTER.match(l) | |
| if m: | |
| unit = f"Unit {m.group(1)}: {m.group(2)}" | |
| if lesson is None: | |
| m = _LESSON_FOOTER.match(l) | |
| if m: | |
| lesson = m.group(1) | |
| if unit and lesson: | |
| break | |
| return unit, lesson | |
| def _page_section_map(pages: list[Document]) -> dict[int, tuple[str | None, str | None]]: | |
| """ | |
| νμ΄μ§λ³ (unit, lesson) λ§΅μ forward-fill λ‘ κ΅¬μ±. | |
| - unit : ν λ² λ±μ₯νλ©΄ λ€μ unitμ΄ λμ¬ λκΉμ§ μ μ§ (μ§/ν λ©΄ λͺ¨λ μ±μ β ~95% 컀λ²λ¦¬μ§) | |
| - lesson: unitμ΄ λ°λλ©΄ 리μ (lessonμ΄ unit κ²½κ³λ₯Ό λμ΄ μλͺ» μ νλλ κ² λ°©μ§) | |
| ν€λ μ²ν¬κ° μμνλ page λ©νκ°(0-indexed)κ³Ό λμΌνκ² λ§μΆλ€. | |
| """ | |
| section: dict[int, tuple[str | None, str | None]] = {} | |
| cur_unit = cur_lesson = None | |
| for i, pg in enumerate(pages): | |
| unit, lesson = _extract_footers(pg.page_content) | |
| if unit and unit != cur_unit: | |
| cur_unit, cur_lesson = unit, None # μ unit μ§μ β lesson 리μ | |
| if lesson: | |
| cur_lesson = lesson | |
| raw_page = pg.metadata.get("page") | |
| page_no = raw_page if isinstance(raw_page, int) else i | |
| section[page_no] = (cur_unit, cur_lesson) | |
| return section | |
| def _toc_section_map(pdf_path: Path) -> dict[int, str]: | |
| """ | |
| PDF λΆλ§ν¬(outline)μ L3 νλͺ©μ 'section'μΌλ‘ λ³΄κ³ νμ΄μ§(0-indexed)λ³λ‘ λ§€ν. | |
| κ³μΈ΅: L1=Unit, L2=Lesson, L3=Section(λ μ¨ λ΄ ν ν½ ν€λ©). | |
| - κ° L3λ μμ νμ΄μ§λΆν° λ€μ toc νλͺ© μ κΉμ§ μ ν¨ (forward-fill) | |
| - Unit/Lesson κ²½κ³(L1Β·L2)λ₯Ό λ§λλ©΄ sectionμ NoneμΌλ‘ 리μ β λ μ¨ κ° μ€μΌ λ°©μ§ | |
| - λ©νλ°μ΄ν° + 컨ν μ€νΈ ν€λ μμͺ½μ μ£λλ€. ν€λ©μ μΉμ 첫 μ²ν¬μλ§ μμΌλ―λ‘, | |
| ν€λμ sectionμ λ£μΌλ©΄ ν€λ© μλ μΉμ μ€κ° μ²ν¬μλ μ£Όμ μ νΈκ° μ€λ¦°λ€(κ²μ 보κ°). | |
| λΆλ§ν¬κ° μκ±°λ μ‘°ν μ€ν¨ μ λΉ λ§΅μ λ°ννλ€(section λ©νλ κ·Έλ₯ μλ΅λ¨). | |
| """ | |
| try: | |
| with fitz.open(str(pdf_path)) as doc: | |
| toc = doc.get_toc() # [[level, title, page(1-indexed)], ...] λ¬Έμ μμ | |
| page_count = doc.page_count | |
| except Exception as e: | |
| print(f"[WARN] {pdf_path.name}: failed to read bookmarks, section metadata skipped ({e})") | |
| return {} | |
| # (μμνμ΄μ§0, section) λ§μ»€ ꡬμ±: L3λ μ€μ , L1/L2λ 리μ | |
| markers: list[tuple[int, str | None]] = [] | |
| cur: str | None = None | |
| for level, title, page1 in toc: | |
| if level == 3: | |
| cur = title.strip() | |
| elif level <= 2: | |
| cur = None | |
| markers.append((page1 - 1, cur)) | |
| # νμ΄μ§λ³ forward-fill (κ°μ νμ΄μ§μ λ§μ»€κ° μ¬λ¬ κ°λ©΄ λ€μͺ½μ΄ μ΄κΉ) | |
| out: dict[int, str] = {} | |
| last: str | None = None | |
| mi = 0 | |
| for p in range(page_count): | |
| while mi < len(markers) and markers[mi][0] <= p: | |
| last = markers[mi][1] | |
| mi += 1 | |
| if last: | |
| out[p] = last | |
| return out | |
| def _strip_footer_noise(page_text: str) -> str: | |
| """λͺ¨λ νμ΄μ§μ λ°λ³΅λλ λ¬λ νΈν°(μ μκΆ/Unit/Lesson μ€)λ₯Ό λ³Έλ¬Έμμ μ κ±°ν΄ μλ² λ© λ Έμ΄μ¦λ₯Ό μ€μ. | |
| μ κ±°λ μ£Όμ μ 보λ λ©νλ°μ΄ν° + 컨ν μ€νΈ ν€λμ ꡬ쑰νλμ΄ λ€μ μ€λ¦¬λ―λ‘ μ 보 μμ€μ μλ€.""" | |
| kept = [ | |
| line for line in page_text.splitlines() | |
| if not (_COPYRIGHT.search(line) or _UNIT_FOOTER.match(line) or _LESSON_FOOTER.match(line)) | |
| ] | |
| return "\n".join(kept) | |
| def _contextual_header(doc_title: str, page: int, | |
| unit: str | None = None, lesson: str | None = None, | |
| section: str | None = None) -> str: | |
| """μλ² λ© ν μ€νΈ μμ prepend ν 컨ν μ€νΈ ν€λ (λ©ν μ 보λ₯Ό μλ² λ©μ λ°μ). | |
| νμ: [Source: <doc> | <unit> | Lesson: <lesson> | Section: <section> | p.N] | |
| (unit/lesson/sectionμ μμ λλ§ ν¬ν¨. sectionμ ν€λ©μ΄ μλ μΉμ μ€κ° μ²ν¬μλ | |
| μ£Όμ μ νΈλ₯Ό μ€μ΄ dense/BM25 λ§€μΉμ 보κ°νλ€.)""" | |
| loc = f"p.{page + 1}" if page >= 0 else "p.?" # pageλ 0-indexed β νμλ 1-indexed | |
| parts = [f"Source: {doc_title}"] | |
| if unit: | |
| parts.append(unit) | |
| if lesson: | |
| parts.append(f"Lesson: {lesson}") | |
| if section: | |
| parts.append(f"Section: {section}") | |
| parts.append(loc) | |
| return f"[{' | '.join(parts)}]" | |
| # ββ Surya OCR (μ΄λ―Έμ§ μ²νΉ) ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _surya_cache: dict = {} | |
| def _init_surya() -> bool: | |
| """Surya OCR λͺ¨λΈμ lazy-load. λ‘λ μ±κ³΅ μ True λ°ν. | |
| μ΄ κ΅¬νμ 0.14+ FoundationPredictor APIλ₯Ό μ¬μ©νλ€(requirements.txt: surya-ocr>=0.14.0, | |
| μ€μΉ νμΈ 0.17.1). 0.17.xμ pad_token_id μ΄μκ° OCR νμ§μ μν₯ μ£Όλ©΄ 0.14~0.16μΌλ‘ | |
| λ΄λ €μ ννλ κ²μ κ³ λ €. | |
| (μ°Έκ³ β κ³Όκ±° 0.6.xλ surya.model.* ν¨μ API(run_ocr), 0.7~0.13μ Recognition/Detection | |
| Predictor ν΄λμ€ APIμλ€. _ocr_pilμ ν΄λΉ λΆκΈ° νμ μ΄ λ¨μ μμΌλ νμ¬ κ²½λ‘λ foundationλ§.) | |
| """ | |
| if "ready" in _surya_cache: | |
| return _surya_cache["ready"] | |
| errors: list[str] = [] | |
| # ββ API #1: surya-ocr 0.14+ (0.17.x νμΈ) β RecognitionPredictorκ° FoundationPredictor νμ ββ | |
| try: | |
| from surya.foundation import FoundationPredictor | |
| from surya.recognition import RecognitionPredictor | |
| from surya.detection import DetectionPredictor | |
| foundation = FoundationPredictor() | |
| _surya_cache["det"] = DetectionPredictor() | |
| _surya_cache["rec"] = RecognitionPredictor(foundation) | |
| _surya_cache["api"] = "foundation" | |
| _surya_cache["ready"] = True | |
| print("[OCR] Surya predictors loaded (foundation API, surya>=0.14)") | |
| return True | |
| except Exception as e: | |
| errors.append(f"foundation API: {type(e).__name__}: {e}") | |
| print(f"[WARN] surya-ocr unavailable. Image OCR will be skipped.") | |
| print(f" Hint: pip install \"surya-ocr>=0.6.0,<0.14.0\"") | |
| for err in errors: | |
| print(f" {err}") | |
| _surya_cache["ready"] = False | |
| return False | |
| def _ocr_pil(pil_img) -> str: | |
| """PIL μ΄λ―Έμ§μ Surya OCRμ μ€νν΄ μΆμΆλ ν μ€νΈλ₯Ό λ°ν. μ€ν¨ μ λΉ λ¬Έμμ΄.""" | |
| try: | |
| api = _surya_cache.get("api") | |
| if api == "foundation": | |
| # 0.14+ __call__: (images, task_names=None, det_predictor=...) β det_predictor νμ | |
| results = _surya_cache["rec"]([pil_img], det_predictor=_surya_cache["det"]) | |
| elif api == "predictor": | |
| # 0.13.x __call__: (images, langs, det_predictor=...) β det_predictor νμ | |
| results = _surya_cache["rec"]([pil_img], [["en"]], det_predictor=_surya_cache["det"]) | |
| else: # function API (surya 0.6.x) | |
| from surya.ocr import run_ocr | |
| results = run_ocr( | |
| [pil_img], [["en"]], | |
| _surya_cache["det_model"], _surya_cache["det_proc"], | |
| _surya_cache["rec_model"], _surya_cache["rec_proc"], | |
| ) | |
| if not results: | |
| return "" | |
| return "\n".join(line.text for line in results[0].text_lines if line.text.strip()) | |
| except Exception as e: | |
| print(f"[WARN] Surya OCR inference failed: {e}") | |
| return "" | |
| def _build_ocr_chunk_id(source_stem: str, index: int) -> str: | |
| """OCR μ²ν¬ μ μ© μμ μ id β ν μ€νΈ μ²ν¬ λ€μμ€νμ΄μ€(_ocr:: μ λ)μ λΆλ¦¬.""" | |
| return f"{source_stem}_ocr::{index:04d}" | |
| def _ocr_contextual_header(doc_title: str, page: int, | |
| unit: str | None = None, lesson: str | None = None, | |
| section: str | None = None) -> str: | |
| """OCR μ²ν¬μ© 컨ν μ€νΈ ν€λ (ν μ€νΈ ν€λμ λμΌνλ '| OCR' νκ·Έ μΆκ°).""" | |
| loc = f"p.{page + 1}" if page >= 0 else "p.?" | |
| parts = [f"Source: {doc_title}"] | |
| if unit: | |
| parts.append(unit) | |
| if lesson: | |
| parts.append(f"Lesson: {lesson}") | |
| if section: | |
| parts.append(f"Section: {section}") | |
| parts.append(loc) | |
| parts.append("OCR") | |
| return f"[{' | '.join(parts)}]" | |
| def ingest_documents(pdf_dir: str = DOCS_DIR, reset: bool = True) -> None: | |
| """ | |
| PDF μΌκ΄ λ‘λ β μ²νΉ β λ©νλ°μ΄ν°/μ μΌ id λΆμ°© β μλ² λ© β ChromaDB μ μ₯. | |
| Args: | |
| pdf_dir: PDF λλ ν 리 (κΈ°λ³Έ data/docs) | |
| reset: Trueμ΄λ©΄ κΈ°μ‘΄ 컬λ μ μ μμ ν μλ‘ μμ± (μ€λ³΅/ꡬ UUID id λ°©μ§) | |
| """ | |
| cfg = get_config() | |
| splitter = RecursiveCharacterTextSplitter( | |
| chunk_size=cfg.rag.chunk_size, | |
| chunk_overlap=cfg.rag.chunk_overlap, | |
| separators=["\n\n", "\n", ".", " "], | |
| ) | |
| embedding = HuggingFaceBgeEmbeddings(model_name="BAAI/bge-m3", encode_kwargs={"batch_size": 32}) | |
| all_docs = [] | |
| all_ids: list[str] = [] | |
| for pdf_path in sorted(Path(pdf_dir).glob("*.pdf")): | |
| loader = PyMuPDFLoader(str(pdf_path), extract_images=False) | |
| pages = loader.load() | |
| if not pages: | |
| print(f"[WARN] {pdf_path.name}: no pages loaded, skipping.") | |
| continue | |
| source = pdf_path.name | |
| source_stem = pdf_path.stem | |
| pdf_meta = pages[0].metadata | |
| doc_title = (pdf_meta.get("title") or "").strip() or source_stem | |
| total_pages = int(pdf_meta.get("total_pages") or len(pages)) | |
| # μ²νΉ 'μ μ' νμ΄μ§ νΈν°μμ unit/lesson μΆμΆ β νμ΄μ§λ³ μ£Όμ λ§΅ ꡬμ±. | |
| # section(L3 ν ν½ ν€λ©)μ PDF λΆλ§ν¬μμ λ³λλ‘ νμ΄μ§λ³ λ§€ν. | |
| # κ·Έ λ€μ λ°λ³΅ νΈν°λ₯Ό λ³Έλ¬Έμμ μ κ±°νκ³ λμ μ²νΉνλ€(μλ² λ© λ Έμ΄μ¦ μ κ±°). | |
| section_map = _page_section_map(pages) | |
| section_toc = _toc_section_map(pdf_path) | |
| for pg in pages: | |
| pg.page_content = _strip_footer_noise(pg.page_content) | |
| chunks = splitter.split_documents(pages) | |
| total_chunks = len(chunks) | |
| for idx, chunk in enumerate(chunks): | |
| raw_page = chunk.metadata.get("page") | |
| page = int(raw_page) if isinstance(raw_page, int) else -1 | |
| original = chunk.page_content | |
| chunk_id = _build_chunk_id(source_stem, idx) | |
| unit, lesson = section_map.get(page, (None, None)) | |
| section = section_toc.get(page) | |
| # λ©νλ°μ΄ν° μ¬κ΅¬μ±: PDF λ‘λκ° μ±μ΄ μ‘λ€ν νλ(creator/producer/β¦)λ λ²λ¦¬κ³ | |
| # μΈμ©Β·νν°λ§Β·νκ°μ μ μ©ν νλλ§ λ¨κΈ΄λ€. (Chromaλ scalar λ©νλ§ νμ©) | |
| chunk.metadata = { | |
| # ββ λ¬Έμ λ¨μ(whole-document) λ©ν ββ | |
| "source": source, | |
| "doc_title": doc_title, | |
| "total_pages": total_pages, | |
| "total_chunks": total_chunks, | |
| # ββ μ²ν¬ λ¨μ(this-chunk) λ©ν ββ | |
| "chunk_id": chunk_id, # 컬λ μ μ μ μ μΌ id == Chroma document id (νκ° κΈ°μ€) | |
| "chunk_index": idx, # λ¬Έμ λ΄ μλ² | |
| "page": page, # 0-indexed (PyMuPDF κ΄λ‘) | |
| "char_count": len(original), | |
| } | |
| # μ£Όμ λ©νλ μΆμΆλ νμ΄μ§μλ§ λΆμ°© (Chromaλ None λ©νλ₯Ό νμ©νμ§ μμΌλ―λ‘ ν€ μ체λ₯Ό μλ΅) | |
| if unit: | |
| chunk.metadata["unit"] = unit | |
| if lesson: | |
| chunk.metadata["lesson"] = lesson | |
| if section: | |
| chunk.metadata["section"] = section # μΈμ©/νν°λ§ + μλ ν€λμ λ°μ | |
| # λ©ν μ 보λ₯Ό μλ² λ© ν μ€νΈμ λ°μ (contextual chunk header) | |
| if cfg.rag.contextual_header: | |
| chunk.page_content = f"{_contextual_header(doc_title, page, unit, lesson, section)}\n\n{original}" | |
| all_docs.append(chunk) | |
| all_ids.append(chunk_id) | |
| last_id = _build_chunk_id(source_stem, total_chunks - 1) | |
| print(f"[OK] {source}: {total_chunks} text chunks ({source_stem}::0000 ~ {last_id})") | |
| # ββ OCR: PDF λ΄μ₯ μ΄λ―Έμ§ β Surya OCR β λ³λ OCR μ²ν¬ βββββββββββββββββ | |
| if cfg.rag.ocr_enabled and _init_surya(): | |
| from PIL import Image as PILImage | |
| ocr_idx = 0 | |
| with fitz.open(str(pdf_path)) as fitz_doc: | |
| for page_idx in range(len(fitz_doc)): | |
| page_obj = fitz_doc[page_idx] | |
| img_list = page_obj.get_images(full=True) | |
| if not img_list: | |
| continue | |
| page_ocr_parts: list[str] = [] | |
| for img_info in img_list: | |
| xref = img_info[0] | |
| try: | |
| pix = fitz.Pixmap(fitz_doc, xref) | |
| # CMYK(n>4) β RGB λ³ν | |
| if pix.n > 4: | |
| pix = fitz.Pixmap(fitz.csRGB, pix) | |
| pil_img = PILImage.open(io.BytesIO(pix.tobytes("png"))) | |
| # 50px λ―Έλ§ μμ΄μ½/μ₯μμ μ μΈ | |
| if pil_img.width < 50 or pil_img.height < 50: | |
| continue | |
| text = _ocr_pil(pil_img) | |
| if text.strip(): | |
| page_ocr_parts.append(text) | |
| except Exception as e: | |
| print(f"[WARN] OCR extract error (page {page_idx}, xref {xref}): {e}") | |
| if not page_ocr_parts: | |
| continue | |
| combined = "\n\n".join(page_ocr_parts) | |
| unit, lesson = section_map.get(page_idx, (None, None)) | |
| section_val = section_toc.get(page_idx) | |
| # κΈ΄ OCR ν μ€νΈλ ν μ€νΈ μ²ν¬μ λμΌν splitterλ‘ λΆν | |
| raw_doc = Document(page_content=combined) | |
| ocr_splits = splitter.split_documents([raw_doc]) | |
| for ocr_chunk in ocr_splits: | |
| ocr_text = ocr_chunk.page_content | |
| cid = _build_ocr_chunk_id(source_stem, ocr_idx) | |
| meta = { | |
| "source": source, | |
| "doc_title": doc_title, | |
| "total_pages": total_pages, | |
| "chunk_id": cid, | |
| "chunk_index": ocr_idx, | |
| "page": page_idx, | |
| "char_count": len(ocr_text), | |
| "source_type": "ocr", | |
| "ocr_engine": "surya", | |
| } | |
| if unit: | |
| meta["unit"] = unit | |
| if lesson: | |
| meta["lesson"] = lesson | |
| if section_val: | |
| meta["section"] = section_val | |
| if cfg.rag.contextual_header: | |
| ocr_text = f"{_ocr_contextual_header(doc_title, page_idx, unit, lesson, section_val)}\n\n{ocr_text}" | |
| all_docs.append(Document(page_content=ocr_text, metadata=meta)) | |
| all_ids.append(cid) | |
| ocr_idx += 1 | |
| if ocr_idx: | |
| print(f"[OCR] {source}: {ocr_idx} OCR chunks added") | |
| if not all_docs: | |
| print("[WARN] No PDFs found. Place PDF files in data/docs/ and re-run.") | |
| return | |
| # κΈ°μ‘΄ 컬λ μ μ΄κΈ°ν (μ€λ³΅ μ μ¬ λ° κ΅¬ UUID id μμ‘΄ λ°©μ§) | |
| if reset: | |
| try: | |
| Chroma( | |
| persist_directory=cfg.paths.chroma_db, | |
| collection_name=cfg.rag.collection_name, | |
| embedding_function=embedding, | |
| ).delete_collection() | |
| print(f"[reset] Existing collection '{cfg.rag.collection_name}' deleted.") | |
| except Exception as e: | |
| print(f"[reset] Could not delete existing collection: {e}") | |
| vectorstore = Chroma.from_documents( | |
| documents=all_docs, | |
| embedding=embedding, | |
| ids=all_ids, # λ©νμ chunk_id μ λμΌν κ°μ vector store id λ‘ μ¬μ© | |
| persist_directory=cfg.paths.chroma_db, | |
| collection_name=cfg.rag.collection_name, | |
| ) | |
| vectorstore.persist() | |
| ocr_count = sum(1 for d in all_docs if d.metadata.get("source_type") == "ocr") | |
| text_count = len(all_docs) - ocr_count | |
| print(f"[DONE] {len(all_docs)} chunks saved to ChromaDB (text: {text_count}, ocr: {ocr_count})") | |
| if __name__ == "__main__": | |
| ingest_documents() | |