This commit is contained in:
hiondal
2025-06-16 03:38:45 +00:00
parent ea36d7b221
commit 59b01d9630
3 changed files with 468 additions and 103 deletions
+302 -57
View File
@@ -147,74 +147,67 @@ class VectorService:
food_category: str,
region: str
) -> Dict[str, Any]:
"""Vector Store를 구축합니다"""
"""Vector Store를 구축합니다 (기존 데이터 업데이트 포함)"""
if not self.is_ready():
raise Exception(f"VectorService가 준비되지 않음: {self.initialization_error}")
try:
logger.info("🚀 Vector Store 구축 시작")
logger.info("🔧 Vector Store 구축 시작 (기존 데이터 업데이트 포함)...")
start_time = datetime.now()
processed_count = 0
documents = []
embeddings = []
metadatas = []
ids = []
# 1단계: 가게 분류 (새 가게 vs 업데이트 vs 중복)
logger.info("🔍 가게 분류 중...")
categorization = self.categorize_stores(review_results, region, food_category)
for store_id, store_info, reviews in review_results:
try:
# 텍스트 추출 및 임베딩 생성
text_for_embedding = extract_text_for_embedding(store_info, reviews)
embedding = self.embedding_model.encode(text_for_embedding)
# 메타데이터 생성
metadata = create_metadata(store_info, food_category, region)
# 문서 ID 생성
document_id = create_store_hash(store_id, store_info.get('name', ''), region)
# 문서 데이터 생성
document_text = combine_store_and_reviews(store_info, reviews)
documents.append(document_text)
embeddings.append(embedding.tolist())
metadatas.append(metadata)
ids.append(document_id)
processed_count += 1
except Exception as e:
logger.warning(f"⚠️ 가게 {store_id} 처리 실패: {e}")
continue
# Vector DB에 저장
if documents:
self.collection.add(
documents=documents,
embeddings=embeddings,
metadatas=metadatas,
ids=ids
# 2단계: 기존 가게 업데이트
update_result = {'updated_count': 0, 'skipped_count': 0}
if categorization['update_stores']:
logger.info(f"🔄 {len(categorization['update_stores'])}개 기존 가게 업데이트 중...")
update_result = self.update_existing_stores(
categorization['update_stores'], food_category, region
)
logger.info(f"✅ Vector Store 구축 완료: {processed_count}개 문서 저장")
return {
'success': True,
'processed_count': processed_count,
'message': f"{processed_count}개 문서가 Vector DB에 저장되었습니다"
}
else:
return {
'success': False,
'error': '저장할 문서가 없습니다',
'processed_count': 0
}
# 3단계: 새 가게 추가
add_result = {'added_count': 0}
if categorization['new_stores']:
logger.info(f"{len(categorization['new_stores'])}개 새 가게 추가 중...")
add_result = self.add_new_stores(
categorization['new_stores'], food_category, region
)
# 4단계: 결과 정리
total_processed = update_result['updated_count'] + add_result['added_count']
execution_time = (datetime.now() - start_time).total_seconds()
result = {
'success': True,
'processed_count': total_processed, # ← 기존 API 호환성 유지
'execution_time': execution_time,
'summary': categorization['summary'],
'operations': {
'new_stores_added': add_result['added_count'],
'existing_stores_updated': update_result['updated_count'],
'update_skipped': update_result.get('skipped_count', 0),
'duplicates_removed': categorization['summary']['duplicate_count']
},
'message': f"✅ Vector DB 처리 완료: 새 가게 {add_result['added_count']}개 추가, "
f"기존 가게 {update_result['updated_count']}개 업데이트"
}
logger.info(f"📊 Vector Store 구축 완료:")
logger.info(f" - 총 처리: {total_processed}")
logger.info(f" - 새 가게: {add_result['added_count']}")
logger.info(f" - 업데이트: {update_result['updated_count']}")
logger.info(f" - 실행 시간: {execution_time:.2f}")
return result
except Exception as e:
logger.error(f"❌ Vector Store 구축 실패: {e}")
return {
'success': False,
'error': str(e),
'processed_count': 0
'processed_count': 0 # ← 기존 API 호환성 유지
}
def search_similar_cases_improved(self, store_id: str, context: str, limit: int = 5) -> Optional[Dict[str, Any]]:
@@ -933,4 +926,256 @@ class VectorService:
'db_path': self.db_path,
'status': 'error',
'error': str(e)
}
}
def get_existing_store_data(self, region: str = None, food_category: str = None) -> Dict[str, Dict[str, Any]]:
"""
Vector DB에서 기존 store 데이터를 가져옵니다.
Args:
region: 지역 필터 (선택사항)
food_category: 음식 카테고리 필터 (선택사항)
Returns:
{store_id: {metadata, document_id}} 형태의 딕셔너리
"""
try:
if not self.is_ready():
logger.warning("⚠️ VectorService가 준비되지 않음")
return {}
# 필터 조건 설정
where_condition = {}
if region and food_category:
where_condition = {
"$and": [
{"region": region},
{"food_category": food_category}
]
}
elif region:
where_condition = {"region": region}
elif food_category:
where_condition = {"food_category": food_category}
# 기존 데이터 조회
if where_condition:
results = self.collection.get(
where=where_condition,
include=['metadatas', 'documents']
)
else:
results = self.collection.get(
include=['metadatas', 'documents']
)
if not results or not results.get('metadatas'):
logger.info("📊 Vector DB에 기존 데이터 없음")
return {}
# store_id별로 기존 데이터 매핑
existing_data = {}
metadatas = results.get('metadatas', [])
ids = results.get('ids', [])
documents = results.get('documents', [])
for i, metadata in enumerate(metadatas):
if metadata and 'store_id' in metadata:
store_id = metadata['store_id']
existing_data[store_id] = {
'metadata': metadata,
'document_id': ids[i] if i < len(ids) else None,
'document': documents[i] if i < len(documents) else None,
'last_updated': metadata.get('last_updated', 'unknown')
}
logger.info(f"📊 기존 Vector DB에서 {len(existing_data)}개 store 데이터 발견")
return existing_data
except Exception as e:
logger.error(f"❌ 기존 store 데이터 조회 실패: {e}")
return {}
def categorize_stores(self, review_results: List[tuple], region: str, food_category: str) -> Dict[str, Any]:
"""
가게들을 새 가게/업데이트 대상/현재 배치 중복으로 분류
"""
# 기존 Vector DB 데이터 조회
existing_data = self.get_existing_store_data(region, food_category)
existing_store_ids = set(existing_data.keys())
# 분류 작업
new_stores = [] # 완전히 새로운 가게
update_stores = [] # 기존 가게 업데이트
current_duplicates = [] # 현재 배치 내 중복
seen_in_batch = set()
for store_id, store_info, reviews in review_results:
# 현재 배치 내 중복 체크
if store_id in seen_in_batch:
current_duplicates.append((store_id, store_info, reviews))
logger.info(f"🔄 현재 배치 중복 발견: {store_id}")
continue
seen_in_batch.add(store_id)
# 기존 DB에 있는지 확인
if store_id in existing_store_ids:
# 업데이트 대상
update_stores.append({
'store_id': store_id,
'new_data': (store_id, store_info, reviews),
'existing_data': existing_data[store_id]
})
logger.info(f"🔄 업데이트 대상: {store_id}")
else:
# 새 가게
new_stores.append((store_id, store_info, reviews))
logger.info(f"✨ 새 가게: {store_id}")
result = {
'new_stores': new_stores,
'update_stores': update_stores,
'current_duplicates': current_duplicates,
'existing_data': existing_data,
'summary': {
'total_input': len(review_results),
'new_count': len(new_stores),
'update_count': len(update_stores),
'duplicate_count': len(current_duplicates),
'will_process': len(new_stores) + len(update_stores)
}
}
logger.info(f"📊 가게 분류 완료:")
logger.info(f" - 새 가게: {len(new_stores)}")
logger.info(f" - 업데이트: {len(update_stores)}")
logger.info(f" - 현재 중복: {len(current_duplicates)}")
return result
def update_existing_stores(self, update_stores: List[Dict], food_category: str, region: str) -> Dict[str, Any]:
"""
기존 가게들을 업데이트
"""
updated_count = 0
skipped_count = 0
update_details = []
for update_item in update_stores:
store_id = update_item['store_id']
new_data = update_item['new_data']
existing_data = update_item['existing_data']
try:
_, store_info, reviews = new_data
# 새 임베딩 및 메타데이터 생성
text_for_embedding = extract_text_for_embedding(store_info, reviews)
if not text_for_embedding or len(text_for_embedding.strip()) < 10:
logger.warning(f"⚠️ {store_id}: 임베딩 텍스트 부족, 업데이트 스킵")
skipped_count += 1
continue
embedding = self.embedding_model.encode(text_for_embedding)
new_metadata = create_metadata(store_info, food_category, region)
document_text = combine_store_and_reviews(store_info, reviews)
# 기존 document_id 사용 (ID 일관성 유지)
document_id = existing_data['document_id']
# Vector DB 업데이트 (ChromaDB에서는 upsert 사용)
self.collection.upsert(
documents=[document_text],
embeddings=[embedding.tolist()],
metadatas=[new_metadata],
ids=[document_id]
)
updated_count += 1
update_details.append({
'store_id': store_id,
'document_id': document_id,
'previous_updated': existing_data['metadata'].get('last_updated', 'unknown'),
'new_updated': new_metadata['last_updated']
})
logger.info(f"{store_id} 업데이트 완료")
except Exception as e:
logger.error(f"{store_id} 업데이트 실패: {e}")
skipped_count += 1
continue
return {
'updated_count': updated_count,
'skipped_count': skipped_count,
'update_details': update_details
}
def add_new_stores(self, new_stores: List[tuple], food_category: str, region: str) -> Dict[str, Any]:
"""
새 가게들을 Vector DB에 추가
"""
if not new_stores:
return {'added_count': 0, 'add_details': []}
documents = []
embeddings = []
metadatas = []
ids = []
added_count = 0
add_details = []
for store_id, store_info, reviews in new_stores:
try:
# 임베딩용 텍스트 생성
text_for_embedding = extract_text_for_embedding(store_info, reviews)
if not text_for_embedding or len(text_for_embedding.strip()) < 10:
logger.warning(f"⚠️ {store_id}: 임베딩 텍스트 부족, 추가 스킵")
continue
# 임베딩 생성
embedding = self.embedding_model.encode(text_for_embedding)
# 메타데이터 생성
metadata = create_metadata(store_info, food_category, region)
# 문서 ID 생성
document_id = create_store_hash(store_id, store_info.get('name', ''), region)
# 문서 데이터 생성
document_text = combine_store_and_reviews(store_info, reviews)
documents.append(document_text)
embeddings.append(embedding.tolist())
metadatas.append(metadata)
ids.append(document_id)
add_details.append({
'store_id': store_id,
'document_id': document_id,
'added_at': metadata['last_updated']
})
added_count += 1
except Exception as e:
logger.warning(f"⚠️ {store_id} 추가 실패: {e}")
continue
# Vector DB에 새 가게들 추가
if documents:
self.collection.add(
documents=documents,
embeddings=embeddings,
metadatas=metadatas,
ids=ids
)
logger.info(f"{added_count}개 새 가게 추가 완료")
return {
'added_count': added_count,
'add_details': add_details
}