release
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
# app/utils/category_utils.py (수정된 버전)
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
def extract_food_category(category_name: str) -> str:
|
||||
"""
|
||||
카테고리명에서 음식 종류를 추출합니다.
|
||||
'음식점 > 한식 > 육류,고기'에서 '한식'을 추출
|
||||
|
||||
Args:
|
||||
category_name: 전체 카테고리명
|
||||
|
||||
Returns:
|
||||
추출된 음식 종류
|
||||
"""
|
||||
if not category_name:
|
||||
return ""
|
||||
|
||||
# '>' 기준으로 분할하고 마지막 바로 전 요소 반환
|
||||
parts = category_name.split('>')
|
||||
if len(parts) >= 2:
|
||||
food_category = parts[-2].strip() # 마지막 바로 전 값 사용
|
||||
return food_category
|
||||
elif len(parts) == 1:
|
||||
return parts[0].strip() # 하나밖에 없으면 그것을 반환
|
||||
|
||||
return category_name.strip()
|
||||
|
||||
def normalize_category(category: str) -> str:
|
||||
"""
|
||||
카테고리를 정규화합니다.
|
||||
|
||||
Args:
|
||||
category: 원본 카테고리
|
||||
|
||||
Returns:
|
||||
정규화된 카테고리
|
||||
"""
|
||||
if not category:
|
||||
return ""
|
||||
|
||||
# 공백 제거 및 소문자 변환
|
||||
normalized = category.strip().lower()
|
||||
|
||||
# 특수문자 제거 (콤마, 슬래시 등은 유지)
|
||||
normalized = re.sub(r'[^\w가-힣,/\s]', '', normalized)
|
||||
|
||||
return normalized
|
||||
|
||||
def is_similar_category(category1: str, category2: str) -> bool:
|
||||
"""
|
||||
두 카테고리가 유사한지 판단합니다.
|
||||
|
||||
Args:
|
||||
category1: 첫 번째 카테고리
|
||||
category2: 두 번째 카테고리
|
||||
|
||||
Returns:
|
||||
유사 여부
|
||||
"""
|
||||
if not category1 or not category2:
|
||||
return False
|
||||
|
||||
# 정규화
|
||||
norm1 = normalize_category(category1)
|
||||
norm2 = normalize_category(category2)
|
||||
|
||||
# 완전 일치
|
||||
if norm1 == norm2:
|
||||
return True
|
||||
|
||||
# 키워드 기반 유사성 검사
|
||||
keywords1 = set(norm1.replace(',', ' ').replace('/', ' ').split())
|
||||
keywords2 = set(norm2.replace(',', ' ').replace('/', ' ').split())
|
||||
|
||||
# 교집합이 하나 이상 있으면 유사한 것으로 판단
|
||||
common_keywords = keywords1.intersection(keywords2)
|
||||
return len(common_keywords) > 0
|
||||
|
||||
def extract_main_category(category_name: str) -> str:
|
||||
"""
|
||||
메인 카테고리를 추출합니다. (음식점 > 한식 에서 '한식' 추출)
|
||||
|
||||
Args:
|
||||
category_name: 전체 카테고리명
|
||||
|
||||
Returns:
|
||||
메인 카테고리
|
||||
"""
|
||||
if not category_name:
|
||||
return ""
|
||||
|
||||
parts = category_name.split('>')
|
||||
if len(parts) >= 2:
|
||||
return parts[1].strip()
|
||||
elif len(parts) == 1:
|
||||
return parts[0].strip()
|
||||
|
||||
return ""
|
||||
|
||||
def build_search_query(region: str, food_category: str) -> str:
|
||||
"""
|
||||
검색 쿼리를 구성합니다. (수정된 버전 - 지역 정보 제외)
|
||||
|
||||
Args:
|
||||
region: 지역 (사용하지 않음)
|
||||
food_category: 음식 카테고리
|
||||
|
||||
Returns:
|
||||
검색 쿼리 문자열 (음식 카테고리만 포함)
|
||||
"""
|
||||
# 콤마와 슬래시를 공백으로 변경하여 검색 키워드 생성
|
||||
search_keywords = food_category.replace(',', ' ').replace('/', ' ')
|
||||
|
||||
# 불필요한 단어 제거
|
||||
stop_words = ['음식점', '요리', '전문점', '맛집']
|
||||
keywords = []
|
||||
|
||||
for keyword in search_keywords.split():
|
||||
keyword = keyword.strip()
|
||||
if keyword and keyword not in stop_words:
|
||||
keywords.append(keyword)
|
||||
|
||||
# 키워드가 없으면 기본 검색어 사용
|
||||
if not keywords:
|
||||
keywords = ['음식점']
|
||||
|
||||
# 🔧 지역 정보는 포함하지 않고 음식 키워드만 반환
|
||||
query = ' '.join(keywords)
|
||||
return query.strip()
|
||||
|
||||
def clean_food_category_for_search(food_category: str) -> str:
|
||||
"""
|
||||
음식 카테고리를 검색용 키워드로 정리합니다.
|
||||
|
||||
Args:
|
||||
food_category: 원본 음식 카테고리
|
||||
|
||||
Returns:
|
||||
정리된 검색 키워드
|
||||
"""
|
||||
if not food_category:
|
||||
return "음식점"
|
||||
|
||||
# 콤마와 슬래시를 공백으로 변경
|
||||
cleaned = food_category.replace(',', ' ').replace('/', ' ')
|
||||
|
||||
# 불필요한 단어 제거
|
||||
stop_words = ['음식점', '요리', '전문점', '맛집']
|
||||
keywords = []
|
||||
|
||||
for keyword in cleaned.split():
|
||||
keyword = keyword.strip()
|
||||
if keyword and keyword not in stop_words:
|
||||
keywords.append(keyword)
|
||||
|
||||
# 키워드가 없으면 기본 검색어 사용
|
||||
if not keywords:
|
||||
return "음식점"
|
||||
|
||||
return ' '.join(keywords)
|
||||
@@ -0,0 +1,194 @@
|
||||
# app/utils/data_utils.py
|
||||
import json
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
def create_store_hash(store_id: str, store_name: str, region: str) -> str:
|
||||
"""
|
||||
가게의 고유 해시를 생성합니다.
|
||||
|
||||
Args:
|
||||
store_id: 가게 ID
|
||||
store_name: 가게명
|
||||
region: 지역
|
||||
|
||||
Returns:
|
||||
생성된 해시값
|
||||
"""
|
||||
combined = f"{store_id}_{store_name}_{region}"
|
||||
return hashlib.md5(combined.encode('utf-8')).hexdigest()
|
||||
|
||||
def combine_store_and_reviews(store_info: Dict[str, Any], reviews: List[Dict[str, Any]]) -> str:
|
||||
"""
|
||||
가게 정보와 리뷰를 결합하여 JSON 문자열을 생성합니다.
|
||||
|
||||
Args:
|
||||
store_info: 가게 정보
|
||||
reviews: 리뷰 목록
|
||||
|
||||
Returns:
|
||||
결합된 JSON 문자열
|
||||
"""
|
||||
combined_data = {
|
||||
"store_info": store_info,
|
||||
"reviews": reviews,
|
||||
"review_summary": generate_review_summary(reviews),
|
||||
"combined_at": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
return json.dumps(combined_data, ensure_ascii=False, separators=(',', ':'))
|
||||
|
||||
def generate_review_summary(reviews: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""
|
||||
리뷰 목록에서 요약 정보를 생성합니다.
|
||||
|
||||
Args:
|
||||
reviews: 리뷰 목록
|
||||
|
||||
Returns:
|
||||
리뷰 요약 정보
|
||||
"""
|
||||
if not reviews:
|
||||
return {
|
||||
"total_reviews": 0,
|
||||
"average_rating": 0.0,
|
||||
"rating_distribution": {},
|
||||
"common_keywords": [],
|
||||
"sentiment_summary": {
|
||||
"positive": 0,
|
||||
"neutral": 0,
|
||||
"negative": 0
|
||||
}
|
||||
}
|
||||
|
||||
# 기본 통계
|
||||
total_reviews = len(reviews)
|
||||
ratings = [review.get('rating', 0) for review in reviews if review.get('rating', 0) > 0]
|
||||
average_rating = sum(ratings) / len(ratings) if ratings else 0.0
|
||||
|
||||
# 별점 분포
|
||||
rating_distribution = {}
|
||||
for rating in ratings:
|
||||
rating_distribution[str(rating)] = rating_distribution.get(str(rating), 0) + 1
|
||||
|
||||
# 키워드 추출 (badges 기반)
|
||||
keyword_counts = {}
|
||||
for review in reviews:
|
||||
badges = review.get('badges', [])
|
||||
for badge in badges:
|
||||
keyword_counts[badge] = keyword_counts.get(badge, 0) + 1
|
||||
|
||||
# 상위 키워드 추출
|
||||
common_keywords = sorted(keyword_counts.items(), key=lambda x: x[1], reverse=True)[:10]
|
||||
common_keywords = [keyword for keyword, count in common_keywords]
|
||||
|
||||
# 감정 분석 (간단한 별점 기반)
|
||||
sentiment_summary = {
|
||||
"positive": len([r for r in ratings if r >= 4]),
|
||||
"neutral": len([r for r in ratings if r == 3]),
|
||||
"negative": len([r for r in ratings if r <= 2])
|
||||
}
|
||||
|
||||
return {
|
||||
"total_reviews": total_reviews,
|
||||
"average_rating": round(average_rating, 2),
|
||||
"rating_distribution": rating_distribution,
|
||||
"common_keywords": common_keywords,
|
||||
"sentiment_summary": sentiment_summary,
|
||||
"has_recent_reviews": any(
|
||||
review.get('date', '') >= datetime.now().strftime('%Y.%m.%d')
|
||||
for review in reviews[-10:] # 최근 10개 리뷰 확인
|
||||
)
|
||||
}
|
||||
|
||||
def extract_text_for_embedding(store_info: Dict[str, Any], reviews: List[Dict[str, Any]]) -> str:
|
||||
"""
|
||||
임베딩을 위한 텍스트를 추출합니다.
|
||||
|
||||
Args:
|
||||
store_info: 가게 정보
|
||||
reviews: 리뷰 목록
|
||||
|
||||
Returns:
|
||||
임베딩용 텍스트
|
||||
"""
|
||||
# 가게 기본 정보
|
||||
store_text = f"가게명: {store_info.get('place_name', '')}\n"
|
||||
store_text += f"카테고리: {store_info.get('category_name', '')}\n"
|
||||
store_text += f"주소: {store_info.get('address_name', '')}\n"
|
||||
|
||||
# 리뷰 내용 요약
|
||||
review_contents = []
|
||||
review_keywords = []
|
||||
|
||||
for review in reviews[:20]: # 최근 20개 리뷰만 사용
|
||||
content = review.get('content', '').strip()
|
||||
if content:
|
||||
review_contents.append(content)
|
||||
|
||||
badges = review.get('badges', [])
|
||||
review_keywords.extend(badges)
|
||||
|
||||
# 리뷰 텍스트 조합
|
||||
if review_contents:
|
||||
store_text += f"리뷰 내용: {' '.join(review_contents[:10])}\n" # 최대 10개 리뷰
|
||||
|
||||
# 키워드 조합
|
||||
if review_keywords:
|
||||
unique_keywords = list(set(review_keywords))
|
||||
store_text += f"키워드: {', '.join(unique_keywords[:15])}\n" # 최대 15개 키워드
|
||||
|
||||
return store_text.strip()
|
||||
|
||||
def create_metadata(store_info: Dict[str, Any], food_category: str, region: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Vector DB용 메타데이터를 생성합니다.
|
||||
|
||||
Args:
|
||||
store_info: 가게 정보
|
||||
food_category: 음식 카테고리
|
||||
region: 지역
|
||||
|
||||
Returns:
|
||||
메타데이터 딕셔너리
|
||||
"""
|
||||
return {
|
||||
"store_id": store_info.get('id', ''),
|
||||
"store_name": store_info.get('place_name', ''),
|
||||
"food_category": food_category,
|
||||
"region": region,
|
||||
"category_name": store_info.get('category_name', ''),
|
||||
"address": store_info.get('address_name', ''),
|
||||
"phone": store_info.get('phone', ''),
|
||||
"place_url": store_info.get('place_url', ''),
|
||||
"x": store_info.get('x', ''), # 좌표를 개별 키로 분리
|
||||
"y": store_info.get('y', ''), # 좌표를 개별 키로 분리
|
||||
"last_updated": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def is_duplicate_store(metadata1: Dict[str, Any], metadata2: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
두 가게가 중복인지 확인합니다.
|
||||
|
||||
Args:
|
||||
metadata1: 첫 번째 가게 메타데이터
|
||||
metadata2: 두 번째 가게 메타데이터
|
||||
|
||||
Returns:
|
||||
중복 여부
|
||||
"""
|
||||
# Store ID 기준 확인
|
||||
if metadata1.get('store_id') and metadata2.get('store_id'):
|
||||
return metadata1['store_id'] == metadata2['store_id']
|
||||
|
||||
# 가게명 + 주소 기준 확인
|
||||
name1 = metadata1.get('store_name', '').strip()
|
||||
name2 = metadata2.get('store_name', '').strip()
|
||||
addr1 = metadata1.get('address', '').strip()
|
||||
addr2 = metadata2.get('address', '').strip()
|
||||
|
||||
if name1 and name2 and addr1 and addr2:
|
||||
return name1 == name2 and addr1 == addr2
|
||||
|
||||
return False
|
||||
Reference in New Issue
Block a user