diff --git a/langchain_rag/utils/embedding.py b/langchain_rag/utils/embedding.py index 13c91dc..caf2318 100644 --- a/langchain_rag/utils/embedding.py +++ b/langchain_rag/utils/embedding.py @@ -1,183 +1,137 @@ +#!/usr/bin/env python3 +# embedding.py """ -3. +S3(또는 로컬) 전처리 텍스트 → 배치 임베딩 → HNSW 인덱스 추가 → JSONL 메타 기록 +--start_entries와 --max_entries 옵션으로 처리 범위를 지정할 수 있습니다. +디버깅용으로 파일 처리 번호와 메모리 로그를 모두 출력합니다. """ -import os -import json -import sys +from __future__ import annotations +import json, os, sys, argparse from pathlib import Path -from typing import List, Tuple - +from typing import Iterable, List + +import faiss import numpy as np from dotenv import load_dotenv from sklearn.preprocessing import normalize -from langchain_community.embeddings import HuggingFaceEmbeddings # KoSimCSE -# from sentence_transformers import SentenceTransformer #gte +import psutil +from langchain_community.embeddings import HuggingFaceEmbeddings from local_storage import LocalStorage -# 프로젝트 루트 디렉토리를 Python 경로에 추가 -CURRENT_DIR = Path(__file__).resolve().parent -PROJECT_ROOT = CURRENT_DIR.parent.parent # utils의 상위 디렉토리의 상위 디렉토리 -sys.path.append(str(PROJECT_ROOT)) - -# store_vector.py에서 필요한 함수들 import -from store_vector import create_hnsw_index, save_metadata - -# 환경 변수 로드 load_dotenv() +HNSW_INDEX_PATH = Path(os.getenv("HNSW_INDEX_PATH", "data/embedding_data/hnsw.index")) +METADATA_PATH = HNSW_INDEX_PATH.with_suffix(".meta.jsonl") + -# 기본 HNSW 인덱스 저장 경로 (환경변수로 재정의 가능) -DEFAULT_HNSW_PATH = Path( - os.getenv("HNSW_INDEX_PATH", "data/embedding_data/hnsw.index") -) -# 기본 메타데이터 저장 경로 -DEFAULT_META_PATH = Path( - os.getenv("METADATA_SAVE_PATH", "data/embedding_data/faiss_metadata.json") -) +def log_mem(stage: str): + m = psutil.Process(os.getpid()).memory_info().rss / (1024**2) + print(f"[MEMORY] {stage}: {m:.2f} MiB") class EmbedFromS3: def __init__( self, - folder_path: str = "pre_processed_data/", # S3 폴더 경로 - model_name: str = "BM-K/KoSimCSE-roberta", # 임베딩 모델명 - # model_name: str = "thenlper/gte-base", - batch_size: int = 64 # 배치 크기 + folder_path: str, + model_name: str, + batch_size: int, + start_entries: int, + max_entries: int | None, + m: int, + ef_construction: int, + ef_search: int, ): - self.folder_path = folder_path - self.batch_size = batch_size - self.localstorage = LocalStorage() - self._model = HuggingFaceEmbeddings(model_name = model_name) - # self._model = SentenceTransformer(model_name) - - # ────────────────────────── - # 임베딩된 내용 vector store에 저장 - # ────────────────────────── - def _save_to_vector_store(self, texts: List[str], vectors: np.ndarray) -> None: - """ - 벡터와 메타데이터를 vector_db에 저장 - """ - create_hnsw_index( - texts=texts, - vectors=vectors - ) - - # ────────────────────────── - # raw 데이터 임베딩 - # ────────────────────────── - def _embed_texts(self, texts: List[str]) -> np.ndarray: - """ - 주어진 텍스트 리스트를 임베딩한 뒤 L2 정규화하여 반환 - """ - embs = self._model.embed_documents(texts) - # embs = self._model.encode(texts, show_progress_bar=False) # gte (sentence_transformers) - return normalize(np.asarray(embs, dtype="float32"), norm="l2") - - # ────────────────────────── - # 1. 단일 파일 임베딩 - # ────────────────────────── - def embed_file(self, file_path: str) -> Tuple[List[str], np.ndarray]: - """ - 1) 단일 JSON/TXT 파일에서 텍스트 추출 - 2) 임베딩 벡터 생성 + 정규화 - 3) HNSW 인덱스와 메타데이터 저장 - """ - # 1) 텍스트 읽기 - texts = ( - self.localstorage.get_all_places_from_json(file_path) - if file_path.lower().endswith(".json") - else self.localstorage.get_all_places_from_txt(file_path) - ) - # 2) 임베딩 - vectors = self._embed_texts(texts) - - # 3) HNSW 인덱스 저장 - self._save_to_vector_store(texts, vectors) - - return texts, vectors - - # ────────────────────────── - # 2. 앞에서 k개 파일 임베딩 - # ────────────────────────── - def embed_k_files(self, k: int) -> Tuple[List[str], np.ndarray]: - """ - 1) S3에서 첫 k개 파일 가져오기 - 2) 순차 임베딩 + 정규화 - 3) HNSW 인덱스와 메타데이터 저장 - """ - keys = self.localstorage.list_first_n_files(self.folder_path, k) - all_texts: List[str] = [] - print(f"[DEBUG] embed_k_files 시작: 처리할 파일 수 = {len(keys)}") - for idx, key in enumerate(keys, start=1): - print(f"[DEBUG] ({idx}/{len(keys)}) 파일 읽는 중: {key}") - # 파일별 텍스트 추출 - texts = self.localstorage.get_all_places_from_json(key) - print(f"[DEBUG] -> 추출된 텍스트 개수: {len(texts)}") - all_texts.extend(texts) - - print(f"[DEBUG] 전체 텍스트 개수: {len(all_texts)} — 임베딩 시작") - # 임베딩 - vectors = self._embed_texts(all_texts) - print(f"[DEBUG] 임베딩 완료: 벡터 shape = {vectors.shape}") - - # HNSW 인덱스 저장 - self._save_to_vector_store(all_texts, vectors) - print(f"[DEBUG] 인덱스 및 메타데이터 저장 완료") - - return all_texts, vectors - - # ────────────────────────── - # 3. 전체 폴더 임베딩 - # ────────────────────────── - def embed_all(self) -> Tuple[List[str], np.ndarray]: - """ - 1) S3 폴더 내 모든 파일 목록 조회 - 2) 파일별로 배치 단위 임베딩 + 정규화 - 3) HNSW 인덱스와 메타데이터 저장 - """ + log_mem("init start") + self.folder_path = folder_path + self.batch_size = batch_size + self.start_entries = start_entries + self.max_entries = max_entries + self.localstorage = LocalStorage() + self._model = HuggingFaceEmbeddings(model_name=model_name) + self._index: faiss.IndexHNSWFlat | None = None + self._m = m + self._ef_construction = ef_construction + self._ef_search = ef_search + log_mem("init complete") + + def _yield_text_batches(self) -> Iterable[List[str]]: file_list = self.localstorage.list_files_in_folder(self.folder_path) - all_texts: List[str] = [] - print(f"[DEBUG] embed_all 시작: 폴더 내 파일 수 = {len(file_list)}") + sent_count = 0 + for idx, fp in enumerate(file_list, start=1): - print(f"[DEBUG] ({idx}/{len(file_list)}) 파일 읽는 중: {fp}") - # 전체 파일 텍스트 추출 - if fp.lower().endswith(".txt"): - texts = self.localstorage.get_all_places_from_predata(fp) - - print(f"[DEBUG] -> 추출된 텍스트 개수: {len(texts)}") - all_texts.extend(texts) - - print(f"[DEBUG] 전체 텍스트 개수: {len(all_texts)} — 임베딩 시작") - # 임베딩 - vectors = self._embed_texts(all_texts) - print(f"[DEBUG] 임베딩 완료: 벡터 shape = {vectors.shape}") - - # HNSW 인덱스 & 메타데이터 저장 - self._save_to_vector_store(all_texts, vectors) - print(f"[DEBUG] 인덱스 및 메타데이터 저장 완료") - - return all_texts, vectors - -# ──────────────────────── -# CLI 테스트 -# ──────────────────────── + if not fp.lower().endswith(".txt"): + continue + texts = self.localstorage.get_all_places_from_predata(fp) + print(f" • ({idx}/{len(file_list)}) {fp}: {len(texts)}개 문장") + + for i in range(0, len(texts), self.batch_size): + batch = texts[i : i + self.batch_size] + sent_count += len(batch) + # skipping already processed + if sent_count <= self.start_entries: + continue + # stop if exceeded + if self.max_entries is not None and sent_count > self.max_entries: + return + yield batch + + def build_hnsw_stream(self) -> None: + total = 0 + METADATA_PATH.parent.mkdir(parents=True, exist_ok=True) + with open(METADATA_PATH, "w", encoding="utf-8") as meta_f: + for batch_texts in self._yield_text_batches(): + log_mem("before embed_documents") + vecs = self._model.embed_documents(batch_texts) + log_mem("after embed_documents") + + vecs = normalize(np.asarray(vecs, dtype="float32"), norm="l2") + log_mem("after normalize") + + if self._index is None: + dim = vecs.shape[1] + self._index = faiss.IndexHNSWFlat(dim, self._m) + self._index.hnsw.efConstruction = self._ef_construction + self._index.hnsw.efSearch = self._ef_search + print(f"[INFO] HNSW 초기화: dim={dim}") + log_mem("after HNSW init") + + log_mem("before index.add") + self._index.add(vecs) + log_mem("after index.add") + + for t in batch_texts: + meta_f.write(json.dumps(t, ensure_ascii=False) + "\n") + + total += len(batch_texts) + log_mem("after cleanup") + + log_mem("before write_index") + HNSW_INDEX_PATH.parent.mkdir(parents=True, exist_ok=True) + faiss.write_index(self._index, str(HNSW_INDEX_PATH)) + log_mem("after write_index") + print(f"[DONE] 인덱스 및 메타데이터 저장 완료 (총 {total} vectors)") + + if __name__ == "__main__": - import argparse - - # parser = argparse.ArgumentParser() - # group = parser.add_mutually_exclusive_group(required=True) - # group.add_argument("--file", help="단일 S3 키 지정") - # group.add_argument("--n", type=int, metavar="N", help="첫 N개 파일 처리") - # group.add_argument("--all", action="store_true", help="폴더 전체 처리") - # args = parser.parse_args() - - emb = EmbedFromS3() - texts, vecs = emb.embed_all() - # if args.file: - # texts, vecs = emb.embed_file(args.file) - # elif args.n is not None: - # texts, vecs = emb.embed_k_files(args.n) - # else: - # texts, vecs = emb.embed_all() - - print(f" 임베딩 및 저장 완료 • 총 {len(texts)}개 • shape={vecs.shape}") \ No newline at end of file + parser = argparse.ArgumentParser() + parser.add_argument("--folder_path", default="pre_processed_data/") + parser.add_argument("--model_name", default="BM-K/KoSimCSE-roberta") + parser.add_argument("--batch_size", type=int, default=64) + parser.add_argument("--start_entries", type=int, default=0, help="건너뛸 문장 수") + parser.add_argument("--max_entries", type=int, default=None, help="처리할 최대 문장 수") + parser.add_argument("--m", type=int, default=32) + parser.add_argument("--ef_construction", type=int, default=40) + parser.add_argument("--ef_search", type=int, default=16) + args = parser.parse_args() + + EmbedFromS3( + folder_path=args.folder_path, + model_name=args.model_name, + batch_size=args.batch_size, + start_entries=args.start_entries, + max_entries=args.max_entries, + m=args.m, + ef_construction=args.ef_construction, + ef_search=args.ef_search, + ).build_hnsw_stream() + diff --git a/langchain_rag/utils/pre_processed_data/preprocessed_data_33.289_126.172.txt b/langchain_rag/utils/pre_processed_data/preprocessed_data_33.289_126.172.txt new file mode 100644 index 0000000..ab706e2 --- /dev/null +++ b/langchain_rag/utils/pre_processed_data/preprocessed_data_33.289_126.172.txt @@ -0,0 +1,35 @@ +주소 : 대한민국 제주특별자치도 서귀포시 대정읍 신도리 2890번지 +위도, 경도 : 33.2868223, 126.1727945 +이름 : 오르간파이프 +전체 평점 : 4.8 +타입 : home_goods_store, store, point_of_interest, establishment +한 줄 요약 리뷰: 다양한 종류의 다육식물을 구할 수 있고 가격이 합리적이며, 판매점의 친절함도 좋다는 긍정적인 리뷰가 많습니다. + +주소 : 대한민국 제주시 +위도, 경도 : 33.28752, 126.16779 +이름 : 한장동 +전체 평점 : 평점 없음 +타입 : transit_station, point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 제주시 특별자치도, 한경면 고산리 3918번지 KR +위도, 경도 : 33.285573, 126.1667494 +이름 : 대해양식 +전체 평점 : 3 +타입 : restaurant, food, point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 제주시 특별자치도, 한경면 고산리 3134-11번지 +위도, 경도 : 33.29269349999999, 126.1780417 +이름 : (주)협성 +전체 평점 : 평점 없음 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 제주시 한경면 고산리 3987 +위도, 경도 : 33.2835332, 126.1678205 +이름 : 보라매수산 +전체 평점 : 3.6 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 이곳은 광어 앙식장으로 인기가 많고 경치가 좋으며, 시기와 조건에 따라 낚시나 보말잡기를 즐길 수 있는 장소이지만, 규모가 넓어 인원 한정으로 운영되며, 일부 관광객은 + diff --git a/langchain_rag/utils/pre_processed_data/preprocessed_data_33.289_126.202.txt b/langchain_rag/utils/pre_processed_data/preprocessed_data_33.289_126.202.txt new file mode 100644 index 0000000..dc2b8da --- /dev/null +++ b/langchain_rag/utils/pre_processed_data/preprocessed_data_33.289_126.202.txt @@ -0,0 +1,14 @@ +주소 : 대한민국 제주시 +위도, 경도 : 33.294876, 126.204179 +이름 : 전답동 +전체 평점 : 평점 없음 +타입 : transit_station, point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주시 +위도, 경도 : 33.295062, 126.203939 +이름 : 고산2리 전답동 +전체 평점 : 평점 없음 +타입 : transit_station, point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + diff --git a/langchain_rag/utils/pre_processed_data/preprocessed_data_33.289_126.242.txt b/langchain_rag/utils/pre_processed_data/preprocessed_data_33.289_126.242.txt new file mode 100644 index 0000000..a535daa --- /dev/null +++ b/langchain_rag/utils/pre_processed_data/preprocessed_data_33.289_126.242.txt @@ -0,0 +1,84 @@ +주소 : 대한민국 제주특별자치도 제주시 한경면 청수리 2755번지 +위도, 경도 : 33.2896049, 126.2420155 +이름 : 열린농장 +전체 평점 : 평점 없음 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 제주시 한경면 청수리 2761-2번지 +위도, 경도 : 33.2891458, 126.2436521 +이름 : 신미영농조합법인 +전체 평점 : 4 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 제주시 한경면 청수리 +위도, 경도 : 33.2892113, 126.2449254 +이름 : 고갯머들 +전체 평점 : 평점 없음 +타입 : lodging, point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 서귀포시 대정읍 무릉인향로53번길 61-70 +위도, 경도 : 33.2872321, 126.2446351 +이름 : 무릉연화303 +전체 평점 : 4.8 +타입 : cafe, point_of_interest, food, store, establishment +한 줄 요약 리뷰: 이 카페는 신선한 파스타, 맛있는 포카치오도우 피자, 친절한 사장님, 깨끗한 실내 환경, 그리고 애견 동반 가능 등으로 인기를 끌고 있어 추천받고 있습니다. + +주소 : 대한민국 제주시 +위도, 경도 : 33.289405, 126.247241 +이름 : 조롱물 +전체 평점 : 평점 없음 +타입 : transit_station, point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 제주시 특별자치도 한경면 청수리 145 3391 +위도, 경도 : 33.290328, 126.2477517 +이름 : 제주국제학교아이비스127 +전체 평점 : 평점 없음 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주시 +위도, 경도 : 33.289469, 126.247991 +이름 : 조롱물 +전체 평점 : 평점 없음 +타입 : transit_station, point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 제주시 한경면 청수리 +위도, 경도 : 33.29161680000001, 126.2472018 +이름 : 석영농장 +전체 평점 : 평점 없음 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 제주시 특별자치도, 한경면 청수리 959-6 +위도, 경도 : 33.290186, 126.2491188 +이름 : 숨 쉬는 고래 제주 +전체 평점 : 3 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 1에서 제시된 내용은 요가 강의가 체계적이지 않고 엉성하다는 평가를 받았다. + +주소 : 대한민국 제주특별자치도 서귀포시 대정읍 무릉리 산29-1 +위도, 경도 : 33.2904803, 126.2493569 +이름 : Ice Palace +전체 평점 : 평점 없음 +타입 : museum, point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주시 한경면 청수리 958번지 1층 제주시 제주특별자치도 KR +위도, 경도 : 33.2910222, 126.2492299 +이름 : 알프스편의점 +전체 평점 : 4 +타입 : store, point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 제주시 한경면 청수리 956-2 +위도, 경도 : 33.290666, 126.2503422 +이름 : 산양리 곶자왈 반딧불 +전체 평점 : 평점 없음 +타입 : park, point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + diff --git a/langchain_rag/utils/pre_processed_data/preprocessed_data_33.289_126.292.txt b/langchain_rag/utils/pre_processed_data/preprocessed_data_33.289_126.292.txt new file mode 100644 index 0000000..65397f9 --- /dev/null +++ b/langchain_rag/utils/pre_processed_data/preprocessed_data_33.289_126.292.txt @@ -0,0 +1,105 @@ +주소 : 대한민국 제주특별자치도 서귀포시 안덕면 서광리 1360 +위도, 경도 : 33.2901135, 126.2963357 +이름 : 서광곶자왈 +전체 평점 : 4 +타입 : park, point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 서귀포시 특별자치도 대정읍 글로벌에듀로 234 +위도, 경도 : 33.290549, 126.2875079 +이름 : Branksome Hall Asia +전체 평점 : 4 +타입 : school, point_of_interest, establishment +한 줄 요약 리뷰: 브랭섬홀은 괜찮은 곳이지만 선생님들이 불친절한 점이 아쉬움을 남긴다. + +주소 : 대한민국 제주특별자치도 서귀포시 특별자치도, 8 Global Edu-ro 260beon-gil, Daejeong-eup +위도, 경도 : 33.2882275, 126.2865348 +이름 : Unifyx +전체 평점 : 평점 없음 +타입 : point_of_interest, store, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 서귀포시 대정읍 구억리 +위도, 경도 : 33.2918946, 126.2874462 +이름 : PAC Auditorium +전체 평점 : 4 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 이 곳의 강당은 아주 좋은 품질을 자랑합니다. + +주소 : 대한민국 제주특별자치도 서귀포시 특별자치도, 안덕면 서광리 1398번지 102호 +위도, 경도 : 33.2906751, 126.2983629 +이름 : 루체 +전체 평점 : 3 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 서귀포시 안덕면 서광리 1384-1번지 +위도, 경도 : 33.2898009, 126.2987159 +이름 : 라모인빌리지 +전체 평점 : 5 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 서귀포시 대정읍 구억리 613번지 +위도, 경도 : 33.2904502, 126.2854768 +이름 : 제이제이케터링(주)비에이취에이 +전체 평점 : 평점 없음 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 서귀포시 대정읍 구억리 613번지 +위도, 경도 : 33.2904502, 126.2854768 +이름 : (주)해울브랭섬홀아시아 +전체 평점 : 평점 없음 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 서귀포시 안덕면 서광리 1398 +위도, 경도 : 33.2906451, 126.2985474 +이름 : 명가공인중개사사무소 +전체 평점 : 3 +타입 : real_estate_agency, point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 서귀포시 대정읍 구억리 산3-2번지 103동 302호 서귀포시 제주특별자치도 KR +위도, 경도 : 33.294567, 126.2900769 +이름 : (주)매산산업개발 +전체 평점 : 평점 없음 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 서귀포시 대정읍 구억리 +위도, 경도 : 33.2890751, 126.2848441 +이름 : KISJ +전체 평점 : 3 +타입 : school, point_of_interest, establishment +한 줄 요약 리뷰: 이 곳은 좋은 추억을 만들 수 있지만, 재방문은 의심스러운 곳이며, 반면 다른 한 사람은 좋은 학교와 서비스를 평가하여 긍정적인 리뷰를 남겼다. + +주소 : 대한민국 제주특별자치도 서귀포시 특별자치도, 안덕면 서광리 1395번지 101동 3층 301호 KR +위도, 경도 : 33.290396, 126.2990479 +이름 : (주)리소프트 +전체 평점 : 평점 없음 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 서귀포시 특별자치도, 에듀시티로 23 +위도, 경도 : 33.29268679999999, 126.2862265 +이름 : 업글북스 +전체 평점 : 평점 없음 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 서귀포시 특별자치도, 안덕면 서광리 2044번지 1층 +위도, 경도 : 33.283667, 126.2961707 +이름 : (주)브이디엘 +전체 평점 : 평점 없음 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 서귀포시 대정읍 구억리 632번지 +위도, 경도 : 33.28784549999999, 126.2846164 +이름 : 구억영농조합법인 +전체 평점 : 평점 없음 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + diff --git a/langchain_rag/utils/pre_processed_data/preprocessed_data_33.289_126.342.txt b/langchain_rag/utils/pre_processed_data/preprocessed_data_33.289_126.342.txt new file mode 100644 index 0000000..b0db4b0 --- /dev/null +++ b/langchain_rag/utils/pre_processed_data/preprocessed_data_33.289_126.342.txt @@ -0,0 +1,119 @@ +주소 : 대한민국 제주특별자치도 서귀포시 안덕면 한창로 365 +위도, 경도 : 33.2917697, 126.3482597 +이름 : 테디밸리 골프&리조트 +전체 평점 : 4.5 +타입 : lodging, point_of_interest, establishment +한 줄 요약 리뷰: 골프장은 코스 관리와 레이아웃이 좋고, 공기도 깨끗한 편이지만, 잔디 상태와 가격에 대한 불만이 있으며, 숙소는 객실이 깨끗하고 조경이 훌륭하며, 음식도 맛있고 가성 + +주소 : 대한민국 제주특별자치도 서귀포시 안덕면 서광리 8-1 +위도, 경도 : 33.2910072, 126.3385544 +이름 : 한송아스콘 +전체 평점 : 평점 없음 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 서귀포시 안덕면 서광리 8-1 +위도, 경도 : 33.2910072, 126.3385544 +이름 : 한송산업 +전체 평점 : 평점 없음 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 서귀포시 안덕면 서광동로34번길 204 +위도, 경도 : 33.2917461, 126.3385075 +이름 : The Premium Membership +전체 평점 : 평점 없음 +타입 : travel_agency, point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 서귀포시 +위도, 경도 : 33.2938403, 126.3448315 +이름 : 해동아스콘(주) +전체 평점 : 평점 없음 +타입 : general_contractor, point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 서귀포시 안덕면 서광리 산6-3 +위도, 경도 : 33.2849639, 126.3373755 +이름 : 서광동리 곶자왈 생태탐방로 +전체 평점 : 4.3 +타입 : park, point_of_interest, establishment +한 줄 요약 리뷰: 제주도의 곶자왈은 조용하고 걷기 좋은 곳으로, 한적하게 길을 걷기에는 더할 나위 없는 산책길이다. + +주소 : 대한민국 서귀포시 안덕면 상창리 2007번지 서귀포시 제주특별자치도 KR +위도, 경도 : 33.2913882, 126.3482769 +이름 : 컬리브라운 +전체 평점 : 평점 없음 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 서귀포시 안덕면 상창리 2007번지 서귀포시 제주특별자치도 KR +위도, 경도 : 33.2913882, 126.3482769 +이름 : 지상낙원 +전체 평점 : 평점 없음 +타입 : jewelry_store, store, point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 서귀포시 안덕면 상창리 2007번지 서귀포시 제주특별자치도 KR +위도, 경도 : 33.2913882, 126.3482769 +이름 : 테디락 +전체 평점 : 평점 없음 +타입 : store, food, point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 서귀포시 안덕면 상창리 2007번지 서귀포시 제주특별자치도 KR +위도, 경도 : 33.2913882, 126.3482769 +이름 : 티워크 +전체 평점 : 평점 없음 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 서귀포시 특별자치도, 테디페리스 +위도, 경도 : 33.2937135, 126.3459347 +이름 : Gemopia +전체 평점 : 평점 없음 +타입 : lodging, point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 서귀포시 안덕면 서광리 1-1 +위도, 경도 : 33.2938403, 126.3457204 +이름 : 행진산업 +전체 평점 : 평점 없음 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 서귀포시 +위도, 경도 : 33.2938403, 126.3460259 +이름 : 산방산업 +전체 평점 : 평점 없음 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 서귀포시 특별자치도 안덕면 관평리 산97-8 +위도, 경도 : 33.2926011, 126.3476551 +이름 : 안덕면 겹동백길 +전체 평점 : 4.4 +타입 : tourist_attraction, point_of_interest, establishment +한 줄 요약 리뷰: 겹동백꽃길은 짧은 길이지만 예쁜 곳으로, 주차공간이 좁고 길 찾기 어려울 수 있지만 무료로 방문 가능하며, 봄에 유채꽃과 함께 아름다운 풍경을 감상할 수 있습니다. + +주소 : 304, 98 bun- gil, 안덕면 서귀포시 대한민국 +위도, 경도 : 33.2926011, 126.3476551 +이름 : Shinwha Entertainment +전체 평점 : 평점 없음 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 리뷰 없음 + +주소 : 대한민국 제주특별자치도 서귀포시 특별자치도, 안덕면 서광동로34길 204 +위도, 경도 : 33.2935656, 126.3470119 +이름 : Teddy Palace +전체 평점 : 4.7 +타입 : point_of_interest, establishment +한 줄 요약 리뷰: 이 리조트는 골프를 좋아하고 조용한 환경을 원하는 사람들에게 적합하며, 추가 시설이 필요할 경우 인근에 위치한 머큐어 호텔을 이용할 수 있습니다. + +주소 : 대한민국 제주특별자치도 서귀포시 안덕면 서광리 1번지 4호 필지 +위도, 경도 : 33.2951762, 126.3428946 +이름 : 테디벨리리조트휴양콘도미니엄 +전체 평점 : 5 +타입 : lodging, point_of_interest, establishment +한 줄 요약 리뷰: 제주 산방산에 위치한 제 골프장은 아름다운 경치와 귀여운 소품들이 특징이며, 소소한 아기자기한 매력이 돋보인다. + diff --git a/langchain_rag/utils/store_vector.py b/langchain_rag/utils/store_vector.py index 923f468..19cc2c9 100644 --- a/langchain_rag/utils/store_vector.py +++ b/langchain_rag/utils/store_vector.py @@ -1,130 +1,113 @@ +#!/usr/bin/env python3 +# store_vector.py """ -4. +Faiss 인덱스 생성 및 메타데이터 저장 함수 모음 +- create_ivfpq_index: IVF-PQ 인덱스 생성 +- create_hnsw_index: HNSW 인덱스 생성 +- save_metadata : 메타데이터 저장 """ import os import json -import faiss -import numpy as np from pathlib import Path from dotenv import load_dotenv -import sys -from typing import List -# 프로젝트 루트 디렉토리를 Python 경로에 추가 -CURRENT_DIR = Path(__file__).resolve().parent -PROJECT_ROOT = CURRENT_DIR.parent.parent # utils의 상위 디렉토리의 상위 디렉토리 -sys.path.append(str(PROJECT_ROOT)) - -# 환경 변수(.env) 로드: 경로 설정 등 -load_dotenv() -# 기본 저장 경로: 환경 변수로 재정의 가능 -FAISS_INDEX_PATH = os.getenv("FAISS_INDEX_PATH", "data/embedding_data/faiss_index.index") -METADATA_SAVE_PATH = os.getenv("METADATA_SAVE_PATH", "data/embedding_data/faiss_metadata.json") - - -def create_flat_index(vectors: np.ndarray, index_path: str) -> None: - """ - Flat(Inner Product) 인덱스 생성 및 디스크에 저장 - - vectors: L2 정규화된 임베딩 행렬 (N x D) - - index_path: 저장할 파일 경로 - """ - dim = vectors.shape[1] - index = faiss.IndexFlatIP(dim) # 원본 벡터를 사용한 내적 탐색 - index.add(vectors) - Path(index_path).parent.mkdir(parents=True, exist_ok=True) - faiss.write_index(index, index_path) +import faiss +import numpy as np +load_dotenv() -def create_ivf_index( - vectors: np.ndarray, - index_path: str, - nlist: int = 100, - nprobe: int = 10 -) -> None: - """ - IVF-Flat 인덱스 생성 및 저장 - - nlist: 코어스 퀀타이저(클러스터) 개수 - - nprobe: 검색 시 탐색할 클러스터 수 - """ - dim = vectors.shape[1] - quantizer = faiss.IndexFlatIP(dim) - index = faiss.IndexIVFFlat(quantizer, dim, nlist, faiss.METRIC_INNER_PRODUCT) - index.train(vectors) # 클러스터 코어스 학습 - index.add(vectors) - index.nprobe = nprobe # 검색 시 k개 클러스터 탐색 - Path(index_path).parent.mkdir(parents=True, exist_ok=True) - faiss.write_index(index, index_path) +FAISS_INDEX_PATH = Path(os.getenv("FAISS_INDEX_PATH", "data/embedding_data/faiss_index.index")) +METADATA_SAVE_PATH = Path(os.getenv("METADATA_SAVE_PATH", "data/embedding_data/faiss_metadata.json")) def create_ivfpq_index( - vectors: np.ndarray, - index_path: str, + vectors: list[np.ndarray] | np.ndarray, + dim: int, nlist: int = 100, m: int = 8, - nprobe: int = 10 + nprobe: int = 10, + max_batches: int | None = None, ) -> None: """ - IVF-PQ 인덱스 생성 및 저장 - - nlist: 클러스터 수 - - m: 서브 양자 개수 (PQ 파티션) - - nprobe: 검색 시 탐색할 클러스터 수 + IVF-PQ 인덱스를 배치 단위로 생성 및 저장합니다. + - vectors: (N,D) ndarray 혹은 [(B,D), ...] 리스트 + - dim : 벡터 차원 + - max_batches: 처리할 배치 수 제한 (없으면 전체) """ - dim = vectors.shape[1] + # 1) 코어스 퀀타이저 학습 quantizer = faiss.IndexFlatIP(dim) - index = faiss.IndexIVFPQ(quantizer, dim, nlist, m, 8, faiss.METRIC_INNER_PRODUCT) - index.train(vectors) - index.add(vectors) + index = faiss.IndexIVFPQ(quantizer, dim, nlist, m, 8, faiss.METRIC_INNER_PRODUCT) index.nprobe = nprobe - Path(index_path).parent.mkdir(parents=True, exist_ok=True) - faiss.write_index(index, index_path) + + # 2) 샘플로 학습 + if isinstance(vectors, list): + sample = np.vstack(vectors[: min(len(vectors), 50_000)]) + else: + sample = vectors[:50_000] + index.train(sample) + + # 3) 배치별로 추가 + if isinstance(vectors, list): + for i, batch in enumerate(vectors, 1): + if max_batches is not None and i > max_batches: + break + index.add(batch) + else: + index.add(vectors) + + # 4) 인덱스 저장 + FAISS_INDEX_PATH.parent.mkdir(parents=True, exist_ok=True) + faiss.write_index(index, str(FAISS_INDEX_PATH)) def create_hnsw_index( - texts: List[str], - vectors: np.ndarray, + texts: list[str], + vectors: np.ndarray | list[np.ndarray], m: int = 32, efConstruction: int = 40, - efSearch: int = 16 + efSearch: int = 16, + max_batches: int | None = None, ) -> None: """ - HNSW 인덱스 생성 및 저장 - - texts: 각 벡터에 대응하는 원본 텍스트 리스트 - - vectors: (N, D) 형태의 numpy.ndarray (N: 문서 수, D: 임베딩 차원) - - index_path: 인덱스를 저장할 경로 (예: '/path/to/my_index.index') - - m: 노드당 연결 수 (기본값: 32) - - efConstruction: 그래프 빌드 시 후보 집합 크기 (기본값: 40) - - efSearch: 검색 시 후보 탐색 폭 (기본값: 16) + HNSW 인덱스를 배치 단위로 생성 및 저장합니다. + - texts : 원본 텍스트 리스트 (메타데이터용) + - vectors: (N,D) ndarray 혹은 [(B,D), ...] 리스트 + - max_batches: 처리할 배치 수 제한 (없으면 전체) """ - # (1) 인덱스 디렉토리 생성 - Path(FAISS_INDEX_PATH).parent.mkdir(parents=True, exist_ok=True) + FAISS_INDEX_PATH.parent.mkdir(parents=True, exist_ok=True) - # (2) 벡터 차원 수 알아내기 - dim = vectors.shape[1] + # 벡터 반복자 준비 + vec_iter = vectors if isinstance(vectors, list) else [vectors] + dim = vec_iter[0].shape[1] - # (3) FAISS HNSW 인덱스 생성 + # HNSW 초기화 index = faiss.IndexHNSWFlat(dim, m) index.hnsw.efConstruction = efConstruction - index.hnsw.efSearch = efSearch + index.hnsw.efSearch = efSearch - # (4) 벡터 추가 - index.add(vectors) + # 배치별로 추가 + for i, batch in enumerate(vec_iter, 1): + if max_batches is not None and i > max_batches: + break + index.add(batch) - # (5) 인덱스 파일 저장 - faiss.write_index(index, FAISS_INDEX_PATH) - - # (6) 메타데이터(텍스트 리스트)를 JSON으로 함께 저장 - metadata_path = Path(FAISS_INDEX_PATH).with_suffix(".meta.json") - with open(metadata_path, "w", encoding="utf-8") as f: - json.dump({"texts": texts}, f, ensure_ascii=False, indent=2) - - print(f"[DEBUG] HNSW index 및 메타데이터 저장 완료 → {FAISS_INDEX_PATH}, {metadata_path}") + # 인덱스 저장 + faiss.write_index(index, str(FAISS_INDEX_PATH)) -def save_metadata(texts: list[str], metadata_path: str) -> None: +def save_metadata( + texts: list[str], + metadata_path: str | None = None, +) -> None: """ - 임베딩된 각 벡터에 대응하는 원본 텍스트 메타데이터를 JSON으로 저장 + 임베딩된 벡터에 대응하는 텍스트 메타데이터를 JSON으로 저장합니다. + - metadata_path: 경로 지정이 없으면 .env 기반 METADATA_SAVE_PATH 사용 """ - Path(metadata_path).parent.mkdir(parents=True, exist_ok=True) - with open(metadata_path, "w", encoding="utf-8") as f: + path = Path(metadata_path) if metadata_path else METADATA_SAVE_PATH + path.parent.mkdir(parents=True, exist_ok=True) + + with open(path, "w", encoding="utf-8") as f: json.dump({"texts": texts}, f, ensure_ascii=False, indent=2) + + print(f"[DONE] 메타데이터 저장 → {path}")