release
This commit is contained in:
parent
ea36d7b221
commit
59b01d9630
208
vector/README.md
208
vector/README.md
@ -566,96 +566,214 @@ curl -X POST "http://vector-api.20.249.191.180.nip.io/action-recommendation" \
|
||||
"success": true,
|
||||
"recommendation": {
|
||||
"summary": {
|
||||
"current_situation": "치킨 전문점으로 강남구 역삼동에 위치, 평균 별점 3.8점으로 동종업체 대비 개선 여지 존재",
|
||||
"current_situation": "일식 업종으로 매출 감소를 겪고 있으며, 경쟁이 치열한 고품질 시장에서 운영 중입니다. 업계 평균 평점은 4.23점이고, 경쟁업체들은 3.9-4.7점 범위의 평점을 보이고 있습니다.",
|
||||
"key_insights": [
|
||||
"배달 서비스 만족도가 경쟁업체 대비 낮음",
|
||||
"신메뉴 출시 주기가 3개월 이상으로 긴 편",
|
||||
"온라인 리뷰 관리 시스템 부재"
|
||||
"업계 공통 이슈로 고객 만족도 양극화 문제 존재",
|
||||
"경쟁업체들은 가성비, 맛, 친절함에서 강점을 보임",
|
||||
"고객 참여도와 일관성 있는 서비스 품질이 성공의 핵심 요소"
|
||||
],
|
||||
"priority_areas": ["배달 품질 개선", "메뉴 혁신"]
|
||||
"priority_areas": [
|
||||
"서비스 일관성 개선",
|
||||
"고객 만족도 안정화"
|
||||
]
|
||||
},
|
||||
"action_plans": {
|
||||
"short_term": [
|
||||
{
|
||||
"title": "배달 포장재 개선",
|
||||
"description": "보온성이 우수한 친환경 포장재로 교체하여 배달 만족도 향상",
|
||||
"expected_impact": "배달 리뷰 평점 0.5점 상승 예상",
|
||||
"timeline": "2주",
|
||||
"cost": "월 50만원"
|
||||
"title": "서비스 표준화 매뉴얼 작성 및 적용",
|
||||
"description": "조리법, 서빙 방식, 고객 응대 등 모든 서비스 과정을 표준화하여 일관성 있는 품질 제공. 직원 교육 실시 및 체크리스트 활용",
|
||||
"expected_impact": "고객 만족도 양극화 해소, 평점 0.3-0.5점 상승 예상",
|
||||
"timeline": "2-4주",
|
||||
"cost": "50-100만원 (교육비, 매뉴얼 제작비)"
|
||||
},
|
||||
{
|
||||
"title": "고객 피드백 수집 시스템 구축",
|
||||
"description": "테이블 QR코드를 통한 실시간 피드백 수집, 불만사항 즉시 대응 체계 마련. 주간 피드백 분석 및 개선사항 도출",
|
||||
"expected_impact": "고객 불만 30% 감소, 재방문율 15% 증가",
|
||||
"timeline": "1-2주",
|
||||
"cost": "30-50만원 (QR코드 제작, 시스템 구축)"
|
||||
}
|
||||
],
|
||||
"mid_term": [
|
||||
{
|
||||
"title": "시즌 한정 메뉴 런칭",
|
||||
"description": "여름 시즌 매운맛 신메뉴 2종 개발 및 SNS 마케팅",
|
||||
"expected_impact": "신규 고객 유입 20% 증가",
|
||||
"timeline": "6주",
|
||||
"cost": "초기 투자 300만원"
|
||||
"title": "메뉴 최적화 및 시그니처 메뉴 개발",
|
||||
"description": "경쟁업체 대비 차별화된 시그니처 메뉴 3-5개 개발. 기존 메뉴 중 인기도 낮은 메뉴 정리하고 가성비 우수 메뉴 강화",
|
||||
"expected_impact": "평균 주문 금액 20% 증가, 고객 재방문율 25% 향상",
|
||||
"timeline": "2-3개월",
|
||||
"cost": "200-300만원 (메뉴 개발, 재료비, 마케팅)"
|
||||
},
|
||||
{
|
||||
"title": "디지털 마케팅 강화",
|
||||
"description": "네이버 플레이스, 구글 마이비즈니스 최적화. SNS 활용한 메뉴 홍보 및 고객 후기 관리. 온라인 주문 시스템 도입",
|
||||
"expected_impact": "온라인 노출 50% 증가, 신규 고객 유입 30% 증가",
|
||||
"timeline": "3-4개월",
|
||||
"cost": "150-250만원 (마케팅비, 시스템 구축비)"
|
||||
}
|
||||
],
|
||||
"long_term": [
|
||||
{
|
||||
"title": "브랜드 리뉴얼",
|
||||
"description": "매장 인테리어 개선 및 브랜드 아이덴티티 강화",
|
||||
"expected_impact": "브랜드 인지도 향상, 객단가 15% 상승",
|
||||
"timeline": "4개월",
|
||||
"cost": "1,500만원"
|
||||
"title": "고객 충성도 프로그램 구축",
|
||||
"description": "멤버십 시스템 도입, 단골 고객 특별 혜택 제공, 생일/기념일 이벤트 운영. 고객 데이터베이스 구축 및 맞춤형 서비스 제공",
|
||||
"expected_impact": "고객 유지율 40% 향상, 월 매출 25-30% 증가",
|
||||
"timeline": "6-8개월",
|
||||
"cost": "300-500만원 (시스템 개발, 운영비)"
|
||||
},
|
||||
{
|
||||
"title": "브랜드 아이덴티티 강화 및 확장 전략",
|
||||
"description": "독특한 브랜드 스토리 개발, 인테리어 리뉴얼, 패키징 디자인 개선. 향후 2호점 진출 또는 프랜차이즈 검토",
|
||||
"expected_impact": "브랜드 인지도 향상, 프리미엄 가격 정책 가능, 사업 확장 기반 마련",
|
||||
"timeline": "8-12개월",
|
||||
"cost": "500-1000만원 (리뉴얼, 브랜딩 비용)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"implementation_tips": [
|
||||
"배달앱 리뷰 모니터링 시스템 도입",
|
||||
"경쟁업체 메뉴 트렌드 주기적 분석",
|
||||
"고객 피드백 기반 개선사항 우선순위 설정"
|
||||
"단기 계획부터 차근차근 실행하되, 서비스 일관성 개선을 최우선으로 진행하세요",
|
||||
"고객 피드백을 적극 수집하고 빠르게 개선사항에 반영하여 고객과의 소통을 강화하세요",
|
||||
"경쟁업체의 강점(가성비, 맛, 친절)을 벤치마킹하되, 차별화 포인트를 명확히 설정하세요"
|
||||
]
|
||||
},
|
||||
"error_message": null,
|
||||
"input_data": {
|
||||
"request_context": {
|
||||
"store_id": "501745730",
|
||||
"owner_request": "매출이 감소하고 있어서 메뉴 개선이 필요합니다.",
|
||||
"analysis_timestamp": "2024-06-16T10:30:00"
|
||||
"owner_request": "매출이 감소하고 있어서 개선 방안이 필요합니다.",
|
||||
"analysis_timestamp": "2025-06-16T03:34:08.622763"
|
||||
},
|
||||
"market_intelligence": {
|
||||
"total_competitors": 7,
|
||||
"total_competitors": 3,
|
||||
"industry_insights": [
|
||||
"업계 전반적으로 고객 만족도 개선 필요",
|
||||
"고성과 업체 3개 벤치마킹 가능"
|
||||
"경쟁이 치열한 고품질 시장",
|
||||
"업계 공통 이슈: 고객 만족도 양극화 (일관성 부족)"
|
||||
],
|
||||
"performance_benchmarks": {
|
||||
"average_rating": 4.1,
|
||||
"review_volume_trend": "증가"
|
||||
"average_rating": 4.23,
|
||||
"rating_range": {
|
||||
"min": 3.9,
|
||||
"max": 4.7
|
||||
},
|
||||
"industry_standard": "Above Average",
|
||||
"review_activity": {
|
||||
"average_reviews": 224,
|
||||
"range": {
|
||||
"min": 103,
|
||||
"max": 412
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"competitive_insights": [
|
||||
{
|
||||
"rank": 1,
|
||||
"store_name": "○○치킨",
|
||||
"similarity_score": 0.892,
|
||||
"store_name": "",
|
||||
"category": "일식",
|
||||
"similarity_score": 0.389,
|
||||
"performance_analysis": {
|
||||
"performance": {
|
||||
"rating": "4.3",
|
||||
"review_count": "156"
|
||||
"rating": "4.1",
|
||||
"review_count": "156",
|
||||
"status": "영업 중"
|
||||
},
|
||||
"feedback": {
|
||||
"positive_aspects": ["맛", "친절", "빠른배달"],
|
||||
"negative_aspects": ["가격"]
|
||||
"positive_aspects": [
|
||||
"가성비",
|
||||
"맛",
|
||||
"분위기"
|
||||
],
|
||||
"negative_aspects": [],
|
||||
"recent_trends": [
|
||||
"최근 만족도 상승"
|
||||
],
|
||||
"rating_pattern": "안정적 고만족"
|
||||
},
|
||||
"business_insights": {
|
||||
"key_finding": "신메뉴 런칭으로 리뷰 급증"
|
||||
"insights": {
|
||||
"competitive_advantage": [
|
||||
"높은 고객 만족도",
|
||||
"활발한 고객 참여",
|
||||
"맛 우수"
|
||||
],
|
||||
"critical_issues": [],
|
||||
"improvement_opportunities": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"rank": 2,
|
||||
"store_name": "",
|
||||
"category": "일식",
|
||||
"similarity_score": 0.347,
|
||||
"performance_analysis": {
|
||||
"performance": {
|
||||
"rating": "4.7",
|
||||
"review_count": "412",
|
||||
"status": "영업 중"
|
||||
},
|
||||
"feedback": {
|
||||
"positive_aspects": [
|
||||
"가성비",
|
||||
"맛",
|
||||
"친절",
|
||||
"분위기"
|
||||
],
|
||||
"negative_aspects": [
|
||||
"고객 만족도 양극화 (일관성 부족)"
|
||||
],
|
||||
"recent_trends": [
|
||||
"최근 만족도 상승"
|
||||
],
|
||||
"rating_pattern": "안정적 고만족"
|
||||
},
|
||||
"insights": {
|
||||
"competitive_advantage": [
|
||||
"높은 고객 만족도",
|
||||
"활발한 고객 참여",
|
||||
"맛 우수",
|
||||
"친절 우수"
|
||||
],
|
||||
"critical_issues": [],
|
||||
"improvement_opportunities": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"rank": 3,
|
||||
"store_name": "",
|
||||
"category": "일식",
|
||||
"similarity_score": 0.314,
|
||||
"performance_analysis": {
|
||||
"performance": {
|
||||
"rating": "3.9",
|
||||
"review_count": "103",
|
||||
"status": "영업 중"
|
||||
},
|
||||
"feedback": {
|
||||
"positive_aspects": [
|
||||
"가성비",
|
||||
"맛",
|
||||
"친절"
|
||||
],
|
||||
"negative_aspects": [],
|
||||
"recent_trends": [],
|
||||
"rating_pattern": "양극화 패턴"
|
||||
},
|
||||
"insights": {
|
||||
"competitive_advantage": [
|
||||
"활발한 고객 참여",
|
||||
"맛 우수",
|
||||
"친절 우수"
|
||||
],
|
||||
"critical_issues": [],
|
||||
"improvement_opportunities": []
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"actionable_recommendations": {
|
||||
"immediate_actions": [
|
||||
"배달 서비스 개선 (업계 5개 업체 공통 문제)"
|
||||
"고객 만족도 양극화 (일관성 부족) 개선 (업계 1개 업체 공통 문제)"
|
||||
],
|
||||
"strategic_improvements": [
|
||||
"메뉴 다양성 확대"
|
||||
],
|
||||
"benchmarking_targets": [
|
||||
"○○치킨: 신메뉴 마케팅 전략"
|
||||
]
|
||||
"strategic_improvements": [],
|
||||
"benchmarking_targets": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,6 +52,8 @@ from app.services.vector_service import VectorService
|
||||
from app.services.claude_service import ClaudeService
|
||||
from app.utils.category_utils import extract_food_category
|
||||
|
||||
import json
|
||||
|
||||
# 로깅 설정
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
@ -434,7 +436,7 @@ async def find_reviews(
|
||||
|
||||
# 동종 업체 리뷰 수집 (본인 가게 제외)
|
||||
similar_store_names = []
|
||||
max_similar_reviews = min(settings.MAX_REVIEWS_PER_RESTAURANT // 2, 20) # 절반 또는 최대 20개
|
||||
max_similar_reviews = settings.MAX_REVIEWS_PER_RESTAURANT
|
||||
for store in similar_stores[:settings.MAX_RESTAURANTS_PER_CATEGORY]: # 환경변수 활용
|
||||
store_info, reviews = await review_service.collect_store_reviews(
|
||||
store.id,
|
||||
|
||||
@ -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}개 문서 저장")
|
||||
# 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
|
||||
)
|
||||
|
||||
return {
|
||||
# 4단계: 결과 정리
|
||||
total_processed = update_result['updated_count'] + add_result['added_count']
|
||||
execution_time = (datetime.now() - start_time).total_seconds()
|
||||
|
||||
result = {
|
||||
'success': True,
|
||||
'processed_count': processed_count,
|
||||
'message': f"{processed_count}개 문서가 Vector DB에 저장되었습니다"
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'success': False,
|
||||
'error': '저장할 문서가 없습니다',
|
||||
'processed_count': 0
|
||||
'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]]:
|
||||
@ -934,3 +927,255 @@ class VectorService:
|
||||
'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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user