Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 106 additions & 13 deletions ELK/app/services/elasticsearch_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,27 +79,120 @@ def is_connected(self) -> bool:
return False

def search_places(self, query: str, max_results: int = 23, user_id: Optional[str] = None) -> List[Dict[str, Any]]:
"""장소 검색 (copy_to 필드와 multi_match로 고도화)"""
"""장소 검색"""

# '맛집' 키워드가 포함된 경우, 쿼리 내에서 '맛집'을 '음식점&카페'로 변환
if '맛집' in query:
query = query.replace('맛집', '음식점&카페')

ALL_CATEGORIES = [
'전시관', '기념관', '전문매장/상가', '5일장', '특산물판매점', '백화점', '상설시장',
'문화전수시설', '문화원', '서양식', '건축/조형물', '음식점&카페', '박물관',
'컨벤션센터', '역사관광지', '복합 레포츠', '공예/공방', '이색음식점', '영화관',
'산업관광지', '중식', '문화시설', '쇼핑', '수상 레포츠', '관광지', '육상 레포츠',
'학교', '관광자원', '스키(보드) 렌탈샵', '대형서점', '휴양관광지', '외국문화원',
'자연관광지', '레포츠', '한식', '일식', '도서관', '체험관광지', '카페/전통찻집',
'면세점', '공연장', '미술관/화랑'
]

# 0. 쿼리 토큰 분리 및 필터링 조건 준비
tokens = query.split()
filter_clause = []

# 카테고리 필터링 로직 추가
temp_query = query
query_categories = []
# 긴 카테고리명부터 확인하여 부분 일치 문제를 방지
sorted_categories = sorted(ALL_CATEGORIES, key=len, reverse=True)
for category in sorted_categories:
if category in temp_query:
query_categories.append(category)
temp_query = temp_query.replace(category, "") # 중복 검사를 피하기 위해 찾은 카테고리 제거

if query_categories:
filter_clause.append({"terms": {"categories.keyword": query_categories}})

# 1. 메인 쿼리를 단계적으로 구성합니다.
search_body = {
"query": {
"multi_match": {
"query": query,
"fields": [
"name^2",
"alias^2",
"categories^2",
"addresses^2",
"content"
"bool": {
"should": [
# 1-1. 가장 정확한 이름 전체 일치 (최고 우선순위)
{
"match_phrase": {
"name": { "query": query, "boost": 30 }
}
},

# 1-2. [수정] '지역+이름/카테고리' 조합 검색 (높은 우선순위)
# cross_fields 타입은 "강남구 파스타" 같은 쿼리에서 모든 단어가 각기 다른 필드에 걸쳐 존재해도 매칭시켜줍니다.
{
"multi_match": {
"query": query,
"fields": ["gu^2", "dong^2", "ro^2", "station^3", "name", "categories"],
"type": "cross_fields",
"operator": "and",
"boost": 20
}
},

# 1-3. '지역+메뉴' 조합을 위한 정확한 검색 (기존 로직)
{
"multi_match": {
"query": query,
"fields": ["name^4", "addresses^5", "station^5", "categories^3", "content^2", "alias^3"],
"type": "cross_fields",
"operator": "and",
"boost": 10
}
},

# 1-4. 오타 교정을 위한 유연한 검색 (기존 로직)
{
"multi_match": {
"query": query,
"fields": ["name", "addresses", "categories", "content", "alias", "station"],
"type": "best_fields",
"fuzziness": "AUTO",
"operator": "or",
"boost": 2
}
}
],
"fuzziness": "AUTO"
"minimum_should_match": 1
}
},
}
}

# '충정로' -> '충정로역' 매칭을 위한 station 필드 부분 일치 점수 추가
# 카테고리를 제외한 나머지 토큰(지역 관련 키워드)으로 쿼리 실행
non_category_query = temp_query.strip()
if non_category_query:
search_body["query"]["bool"]["should"].append(
{
"match_phrase_prefix": {
"station": {
"query": non_category_query,
"boost": 15
}
}
}
)

# 2. 준비된 필터링 조건이 있으면 쿼리에 추가합니다.
if filter_clause:
search_body["query"]["bool"]["filter"] = filter_clause

# 3. 최종 쿼리 조립
final_query = {
**search_body,
"min_score": 20,
"sort": [{"_score": {"order": "desc"}}],
"size": max_results,
"_source": ["uuid", "name", "category", "subcategory"]
}

response = self.es.search(index=self.index_name, body=search_body)
response = self.es.search(index=self.index_name, body=final_query)

hits = response['hits']['hits']
places = [
Expand Down
11 changes: 9 additions & 2 deletions MLOps/app/routers/crowd.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@
print("PRED_PATH is not set")

@router.get("/crowd", response_model=CrowdResponse)
def get_crowd_prediction(hour: int):
def get_crowd_prediction(hour: int, area: str = "all"):
"""
## 시간대별 혼잡도 예측 결과 조회 API
- **hour**: 조회할 예측 시간 (1, 2, 3, 6, 12 중 하나)
- **area**: 조회할 지역 (all, 서울, 경기, 인천, 강원, 충청, 전라, 경상, 제주 중 하나)
"""
try:
# 1. 대상 파일명 생성
Expand All @@ -31,7 +32,13 @@ def get_crowd_prediction(hour: int):

df = pd.read_csv(file_path)

# 3. 응답 데이터 형식으로 변환
# 3. 'area' 파라미터에 따른 데이터 필터링
if area != "all":
df = df[df['AREA_NM'] == area]
if df.empty:
raise HTTPException(status_code=404, detail=f"No data found for area: {area}")

# 4. 응답 데이터 형식으로 변환
crowd_info_list = []
for _, row in df.iterrows():
crowd_info_list.append(CrowdInfo(
Expand Down
3 changes: 2 additions & 1 deletion MLOps/app/services/langchain_agent_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ async def search_course(query: str, region: str, categories: List[str]) -> List[
essential_data.append({
"uuid": meta.get("uuid"),
"name": meta.get("name"),
"area": meta.get("area"),
"address": meta.get("address"),
"content": meta.get("content", "")
})
Expand Down Expand Up @@ -160,7 +161,7 @@ def _create_prompt_template(self):
코스 추천: 특별한 요청이 없으면 카테고리당 1곳을 추천하고, 코스 전체에 포함되는 장소는 최대 6개로 제한해.
장소 추천: 특정한 장소만 추천해줄 때는 최대 6개로 제한해.
내용 형식:
각 장소는 반드시 예시와 같은 형식으로 작성해.
각 장소는 반드시 예시와 같은 형식으로 작성해. <n>, <br>를 제외하고 절대 마크다운 형식을 사용하지 마.
(예시: <br>1. 상호명 - 주소<n>설명<br>2. 상호명 - 주소<n>설명<br>3. 상호명 - 주소<n>설명)
**필수 규칙**
- 지역과 카테고리의 정보가 충족되면, 즉시 코스나 장소를 추천하는 응답을 해야해.
Expand Down
4 changes: 2 additions & 2 deletions data/chroma_db_bge.dvc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
outs:
- md5: 604fc684f03c98f8c31ef3a0e13a544b.dir
- md5: 11f5acce78294ca54c6aba954eaf1965.dir
nfiles: 6
hash: md5
path: chroma_db_bge
size: 92402917
size: 92755173