release
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user