Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ FastAPI와 MySQL을 사용한 카카오톡 챗봇 개발을 위한 템플릿 프
│ ├── test_get_my_category_list.py
│ ├── test_get_news_recommendations.py
│ ├── test_news_client.py
│ ├── test_news_DB_search.py
│ │── test_user_delete_favorite.py
│ └── utils/
│ ├── response_builder/
Expand Down
125 changes: 125 additions & 0 deletions app/routers/news.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import logging

from elasticsearch import Elasticsearch
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session

Expand All @@ -16,6 +17,7 @@

router = APIRouter()
db_dependency = Depends(db_manager.get_db) # 전역 변수로 설정
es = Elasticsearch("http://elasticsearch:9200")


@router.get("/recommend")
Expand Down Expand Up @@ -91,3 +93,126 @@ def get_news_recommendations(
return {
"results": results
}

@router.get("/DB_search")
def search_news_by_keyword(
user_id: str = Query(..., description="사용자 ID"),
keyword: str = Query(..., description="검색할 카테고리 키워드 (예: '보건', '환경')"),
limit: int = Query(10, ge=1, le=100, description="검색 결과 최대 개수 (기본값: 10, 최대: 100)"),
db: Session = db_dependency
):
"""
사용자가 입력한 키워드를 기반으로 가장 유사한 뉴스 카테고리를 찾고, 해당 카테고리에 속한 뉴스 기사를 반환합니다.

Args:
user_id (str): 검색을 수행할 사용자 ID.
keyword (str): 검색 키워드 (카테고리명).
limit (int): 반환할 채용 공고 수 (기본값: 10, 최대 100).
db (Session): 데이터베이스 세션 객체.

Returns:
dict: {
"matched_category": str,
"results": List[dict]
}
"""
# ✅ 1. 사용자 존재 여부 확인
user = db.query(Users).filter(Users.user_id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")

try:
# ✅ 2-1. match_phrase_prefix로 후보군 검색 (자동완성 역할)
prefix_result = es.search(
index="categories",
body={
"size": 10,
"query": {
"bool": {
"must": {
"match_phrase_prefix": {
"category_name": {"query": keyword}
}
},
"filter": {
"term": {
"feature": "news"
}
}
}
}
}
)
prefix_hits = prefix_result.get("hits", {}).get("hits", [])
if not prefix_hits:
# 후보군 없으면 바로 기타 처리
matched_category = "기타"
category_id = 0
else:
candidate_names = [hit["_source"]["category_name"] for hit in prefix_hits]

# ✅ 2-2. BM25 기반 match 쿼리로 후보군 중 가장 유사한 카테고리 검색
bm25_result = es.search(
index="categories",
body={
"size": 1,
"query": {
"bool": {
"must": {
"match": {
"category_name": {
"query": keyword,
"operator": "and"
}
}
},
"filter": {
"terms": {
"category_name.keyword": candidate_names # 후보군 필터링
}
}
}
}
}
)
bm25_hits = bm25_result.get("hits", {}).get("hits", [])
if bm25_hits:
matched_source = bm25_hits[0]["_source"]
matched_category = matched_source["category_name"]
category_id = matched_source["category_id"]
else:
# BM25가 후보군 중 적합한 것을 못 찾으면 prefix 후보 중 1순위 반환
matched_category = prefix_hits[0]["_source"]["category_name"]
category_id = prefix_hits[0]["_source"]["category_id"]

except ConnectionError as e:
raise HTTPException(status_code=500, detail="Elasticsearch 연결 실패") from e
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e

# ✅ 3. 해당 카테고리의 뉴스 조회
news_list = (
db.query(News)
.filter(News.category_id == category_id)
.order_by(News.publish_date.desc())
.limit(limit)
.all()
)

# ✅ 4. 뉴스 결과 정리
results = [
{
"title": news.title,
"source": news.source,
"publish_date": news.publish_date.isoformat(), # date → 문자열 (ISO 포맷)
"url": news.url,
"original_url": news.original_url
}
for news in news_list
]

# ✅ 5. 최종 응답 반환
return {
"matched_category": matched_category,
"results": results
}
Loading