kcsc-mcp / src /utils /vector_db_utils.py
nicefree19's picture
Upload 105 files
0ee3c92 verified
Raw
History Blame Contribute Delete
9.86 kB
"""
๋ฒกํ„ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ จ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜
"""
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