Spaces:
Sleeping
Sleeping
| """ | |
| ๋ฒกํฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ด๋ จ ์ ํธ๋ฆฌํฐ ํจ์ | |
| """ | |
| import os | |
| import json | |
| import logging | |
| import re | |
| from datetime import datetime | |
| from typing import List, Dict, Any, Tuple, Optional | |
| from src import config | |
| from src.utils.exceptions import VectorDBError, DataProcessingError | |
| logger = config.setup_logger("vector_db_utils") | |
| def get_latest_data_files(prefix: str, data_dir: str = None) -> List[str]: | |
| """ | |
| ํน์ ์ ๋์ฌ์ ์ผ์นํ๋ ์ต์ ๋ฐ์ดํฐ ํ์ผ ๋ชฉ๋ก ๋ฐํ | |
| Args: | |
| prefix: ํ์ผ ์ ๋์ฌ (์: 'KDS_details') | |
| data_dir: ๋ฐ์ดํฐ ๋๋ ํ ๋ฆฌ ๊ฒฝ๋ก (๊ธฐ๋ณธ๊ฐ: config.DATA_DIR) | |
| Returns: | |
| ์ต์ ๋ฐ์ดํฐ ํ์ผ ๊ฒฝ๋ก ๋ชฉ๋ก | |
| Raises: | |
| DataProcessingError: ํ์ผ์ ์ฐพ์ ์ ์๋ ๊ฒฝ์ฐ | |
| """ | |
| data_dir = data_dir or config.DATA_DIR | |
| files = {} | |
| try: | |
| for filename in os.listdir(data_dir): | |
| if not filename.startswith(prefix) or not filename.endswith('.json'): | |
| continue | |
| parts = filename.split('_') | |
| if len(parts) < 2: | |
| continue | |
| doc_type = parts[0] | |
| timestamp = parts[-1].replace('.json', '') | |
| if doc_type not in files or timestamp > files[doc_type][1]: | |
| files[doc_type] = (os.path.join(data_dir, filename), timestamp) | |
| if not files: | |
| logger.warning(f"'{prefix}' ์ ๋์ฌ๋ก ์์ํ๋ ํ์ผ์ด '{data_dir}'์ ์์ต๋๋ค.") | |
| return [] | |
| return [file_info[0] for file_info in files.values()] | |
| except Exception as e: | |
| raise DataProcessingError( | |
| message=f"๋ฐ์ดํฐ ํ์ผ ๊ฒ์ ์ค ์ค๋ฅ ๋ฐ์: {str(e)}", | |
| data_type=prefix | |
| ) | |
| def load_json_data(filepath: str) -> List[Dict[str, Any]]: | |
| """ | |
| JSON ํ์ผ์์ ๋ฐ์ดํฐ ๋ก๋ | |
| Args: | |
| filepath: JSON ํ์ผ ๊ฒฝ๋ก | |
| Returns: | |
| ๋ก๋๋ ๋ฐ์ดํฐ ๋ฆฌ์คํธ | |
| Raises: | |
| DataProcessingError: ํ์ผ ๋ก๋ ์ค ์ค๋ฅ ๋ฐ์ ์ | |
| """ | |
| try: | |
| logger.info(f"๋ฐ์ดํฐ ํ์ผ ๋ก๋ ์ค: {filepath}") | |
| with open(filepath, 'r', encoding='utf-8') as f: | |
| data = json.load(f) | |
| logger.info(f"๋ฐ์ดํฐ ๋ก๋ ์๋ฃ: {len(data)}๊ฐ ํญ๋ชฉ") | |
| return data | |
| except Exception as e: | |
| raise DataProcessingError( | |
| message=f"๋ฐ์ดํฐ ํ์ผ '{filepath}' ๋ก๋ ์ค ์ค๋ฅ ๋ฐ์: {str(e)}", | |
| data_type="json" | |
| ) | |
| def determine_optimal_chunk_size(text: str, min_size: int = 200, max_size: int = 2000) -> int: | |
| """ | |
| ๋ฌธ์ ๋ด์ฉ์ ๋ฐ๋ผ ์ต์ ์ ์ฒญํฌ ํฌ๊ธฐ ๊ฒฐ์ | |
| Args: | |
| text: ๋ฌธ์ ๋ด์ฉ ํ ์คํธ | |
| min_size: ์ต์ ์ฒญํฌ ํฌ๊ธฐ | |
| max_size: ์ต๋ ์ฒญํฌ ํฌ๊ธฐ | |
| Returns: | |
| ์ต์ ์ ์ฒญํฌ ํฌ๊ธฐ | |
| """ | |
| text_length = len(text) | |
| if text_length <= min_size: | |
| return text_length | |
| if text_length < 1000: | |
| return min(text_length, max_size) | |
| elif text_length < 5000: | |
| return min(800, max_size) | |
| elif text_length < 10000: | |
| return min(1000, max_size) | |
| else: | |
| return min(1500, max_size) | |
| def split_text_into_chunks( | |
| text: str, | |
| chunk_size: int = None, | |
| overlap: int = None, | |
| respect_paragraphs: bool = True | |
| ) -> List[str]: | |
| """ | |
| ํ ์คํธ๋ฅผ ์ฒญํฌ๋ก ๋ถํ | |
| Args: | |
| text: ๋ถํ ํ ํ ์คํธ | |
| chunk_size: ์ฒญํฌ ํฌ๊ธฐ | |
| overlap: ์ฒญํฌ ๊ฐ ๊ฒน์น๋ ๋ฌธ์ ์ | |
| respect_paragraphs: ๊ฐ๋ฅํ ํ ๋ฌธ๋จ ๊ฒฝ๊ณ๋ฅผ ์กด์คํ ์ง ์ฌ๋ถ | |
| Returns: | |
| ๋ถํ ๋ ํ ์คํธ ์ฒญํฌ ๋ชฉ๋ก | |
| """ | |
| if not text: | |
| return [] | |
| chunk_size = chunk_size or config.CHUNK_SIZE | |
| overlap = overlap or config.CHUNK_OVERLAP | |
| if chunk_size > len(text): | |
| return [text] | |
| chunks = [] | |
| if respect_paragraphs: | |
| paragraphs = text.split('\n\n') | |
| current_chunk = "" | |
| for para in paragraphs: | |
| if not para.strip(): | |
| continue | |
| if len(current_chunk) + len(para) > chunk_size: | |
| if current_chunk: | |
| chunks.append(current_chunk) | |
| if len(para) > chunk_size: | |
| para_chunks = _split_by_chars(para, chunk_size, overlap) | |
| chunks.extend(para_chunks) | |
| current_chunk = para_chunks[-1][-overlap:] if overlap > 0 and para_chunks else "" | |
| else: | |
| current_chunk = para | |
| else: | |
| if current_chunk: | |
| current_chunk += "\n\n" + para | |
| else: | |
| current_chunk = para | |
| if current_chunk: | |
| chunks.append(current_chunk) | |
| else: | |
| chunks = _split_by_chars(text, chunk_size, overlap) | |
| return chunks | |
| def _split_by_chars(text: str, chunk_size: int, overlap: int) -> List[str]: | |
| """ | |
| ํ ์คํธ๋ฅผ ๋ฌธ์ ๋จ์๋ก ์ฒญํฌ๋ก ๋ถํ | |
| """ | |
| chunks = [] | |
| start = 0 | |
| while start < len(text): | |
| end = min(start + chunk_size, len(text)) | |
| if end < len(text): | |
| sentence_end = max( | |
| text.rfind('. ', start, end), | |
| text.rfind('.\n', start, end), | |
| text.rfind('? ', start, end), | |
| text.rfind('?\n', start, end), | |
| text.rfind('! ', start, end), | |
| text.rfind('!\n', start, end) | |
| ) | |
| if sentence_end == -1 or (sentence_end - start) < (chunk_size // 2): | |
| sentence_end = text.rfind(' ', start, end) | |
| if sentence_end != -1 and (sentence_end - start) >= (chunk_size // 3): | |
| end = sentence_end + 1 | |
| chunk = text[start:end].strip() | |
| if chunk: | |
| chunks.append(chunk) | |
| start = max(start + 1, end - overlap) | |
| if start >= end: | |
| start = end | |
| return chunks | |
| def check_index_status(collection_name: str, num_docs: int) -> Tuple[bool, str]: | |
| """ | |
| ์ธ๋ฑ์ค ์ํ ํ์ธ | |
| Args: | |
| collection_name: ์ปฌ๋ ์ ์ด๋ฆ | |
| num_docs: ๋ฌธ์ ์ | |
| Returns: | |
| (์ฑ๊ณต ์ฌ๋ถ, ์ํ ๋ฉ์์ง) ํํ | |
| """ | |
| if num_docs == 0: | |
| return False, f"'{collection_name}' ์ปฌ๋ ์ ์ ๋ฌธ์๊ฐ ์์ต๋๋ค." | |
| return True, f"'{collection_name}' ์ปฌ๋ ์ ์ {num_docs}๊ฐ ๋ฌธ์๊ฐ ์์ต๋๋ค." | |
| def structured_chunking(text: str, base_metadata: Dict[str, Any] = None, chunk_size: int = 2000) -> List[Dict[str, Any]]: | |
| """ | |
| KDS/KCS ํ์ค ๋ฌธ์๋ฅผ ์น์ ๋จ์๋ก ์ฒญํนํฉ๋๋ค. | |
| ์ ๋ต: | |
| - ์ต์์ ์กฐํญ(1., 2., 3. ๋ฑ ํ~๋ ์๋ฆฌ ์ซ์ + ์ + ๊ณต๋ฐฑ)์ ์น์ ๊ฒฝ๊ณ๋ก ์ธ์ | |
| - ์น์ ์ด chunk_size๋ฅผ ์ด๊ณผํ ๊ฒฝ์ฐ์๋ง ์ถ๊ฐ ๋ถํ | |
| - ๋ฌธ์๋น ๋ณดํต 5~15๊ฐ ์ฒญํฌ ์์ฑ (๋ชฉํ: ์ ์ฒด ~5~10๋ง ์ฒญํฌ) | |
| Args: | |
| text: ๋ถํ ํ ํ์ค ๋ฌธ์ ์ ์ฒด ํ ์คํธ | |
| base_metadata: ๊ธฐ๋ณธ ๋ฉํ๋ฐ์ดํฐ (๋ฌธ์๋ช , ์ฝ๋ ๋ฑ) | |
| chunk_size: ์ต๋ ์ฒญํฌ ํฌ๊ธฐ (๊ธฐ๋ณธ 2000์) | |
| Returns: | |
| ๋ฉํ๋ฐ์ดํฐ๊ฐ ํฌํจ๋ ์ฒญํฌ ๋์ ๋๋ฆฌ ๋ชฉ๋ก | |
| """ | |
| if not text: | |
| return [] | |
| base_metadata = base_metadata or {} | |
| chunks = [] | |
| # ์ต์์ ์กฐํญ ๊ฒฝ๊ณ: ์ค ์์์ "1. " "10. " ํ์์ ํ๋ ์๋ฆฌ ์ซ์ | |
| top_section_pattern = re.compile(r"^(\d{1,2})\.\s+\S") | |
| lines = text.split('\n') | |
| # ์ต์์ ์น์ ๊ฒฝ๊ณ ์ฐพ๊ธฐ | |
| section_starts: List[Tuple[int, str]] = [] | |
| for i, line in enumerate(lines): | |
| stripped = line.strip() | |
| if stripped and top_section_pattern.match(stripped): | |
| section_starts.append((i, stripped)) | |
| def make_chunk(content: str, section_heading: str, chunk_idx: int = 0) -> Dict[str, Any]: | |
| prefix = f"[{section_heading}]\n" if section_heading else "" | |
| return { | |
| "page_content": (prefix + content).strip(), | |
| "metadata": { | |
| **base_metadata, | |
| "element_type": "section", | |
| "heading_context": section_heading, | |
| "chunk_index": chunk_idx | |
| } | |
| } | |
| def add_section_as_chunks(section_text: str, heading: str): | |
| """์น์ ์ด chunk_size ์ดํ๋ฉด 1์ฒญํฌ, ์ด๊ณผ๋ฉด ๋จ๋ฝ ๋จ์ ๋ถํ """ | |
| if not section_text.strip(): | |
| return | |
| if len(section_text) <= chunk_size: | |
| chunks.append(make_chunk(section_text, heading)) | |
| else: | |
| sub = split_text_into_chunks(section_text, chunk_size=chunk_size, overlap=200, respect_paragraphs=True) | |
| for i, sc in enumerate(sub): | |
| chunks.append(make_chunk(sc, heading, chunk_idx=i)) | |
| if not section_starts: | |
| # ์น์ ๊ตฌ๋ถ์ด ์์ผ๋ฉด ์ ์ฒด๋ฅผ chunk_size ๋จ์๋ก ๋ถํ | |
| sub = split_text_into_chunks(text, chunk_size=chunk_size, overlap=200, respect_paragraphs=True) | |
| for i, sc in enumerate(sub): | |
| chunks.append(make_chunk(sc, "", chunk_idx=i)) | |
| return chunks | |
| # ์น์ ์ด์ ๋จธ๋ฆฌ๋ง ์ฒ๋ฆฌ | |
| if section_starts[0][0] > 0: | |
| preamble = '\n'.join(lines[:section_starts[0][0]]).strip() | |
| if preamble: | |
| add_section_as_chunks(preamble, "๋จธ๋ฆฌ๋ง") | |
| # ๊ฐ ์น์ ์ฒ๋ฆฌ | |
| for idx, (start_line, heading) in enumerate(section_starts): | |
| end_line = section_starts[idx + 1][0] if idx + 1 < len(section_starts) else len(lines) | |
| section_text = '\n'.join(lines[start_line:end_line]).strip() | |
| if section_text: | |
| add_section_as_chunks(section_text, heading) | |
| return chunks | |