import math from bson import ObjectId from fastapi import HTTPException from motor.motor_asyncio import AsyncIOMotorDatabase from app.models.todo import TodoResponse, SearchResponse class SearchService: """MongoDB text index 기반 검색 로직""" def __init__(self, db: AsyncIOMotorDatabase): self.collection = db["todos"] self.categories = db["categories"] async def _populate_categories_bulk(self, todo_docs: list[dict]) -> list[TodoResponse]: """여러 Todo 문서의 카테고리 정보를 일괄 조회하여 TodoResponse 리스트를 반환한다.""" # 카테고리 ID 수집 cat_ids = set() for doc in todo_docs: cat_id = doc.get("category_id") if cat_id: try: cat_ids.add(ObjectId(cat_id)) except Exception: pass # 카테고리 일괄 조회 cat_map: dict[str, dict] = {} if cat_ids: async for cat in self.categories.find({"_id": {"$in": list(cat_ids)}}): cat_map[str(cat["_id"])] = cat # TodoResponse 변환 result = [] for doc in todo_docs: cat_id = str(doc["category_id"]) if doc.get("category_id") else None cat = cat_map.get(cat_id) if cat_id else None result.append(TodoResponse( id=str(doc["_id"]), title=doc["title"], content=doc.get("content"), completed=doc["completed"], priority=doc["priority"], category_id=cat_id, category_name=cat["name"] if cat else None, category_color=cat["color"] if cat else None, tags=doc.get("tags", []), due_date=doc.get("due_date"), created_at=doc["created_at"], updated_at=doc["updated_at"], )) return result async def search(self, query: str, page: int = 1, limit: int = 20) -> SearchResponse: """F-017: 전문 검색 MongoDB text index를 활용한 전문 검색, text score 기준 정렬, 페이지네이션 """ if not query or not query.strip(): raise HTTPException(status_code=422, detail="검색어를 입력해주세요") query = query.strip() # MongoDB text search search_filter = {"$text": {"$search": query}} # 총 개수 조회 total = await self.collection.count_documents(search_filter) # text score 기준 정렬 + 페이지네이션 skip = (page - 1) * limit cursor = self.collection.find( search_filter, {"score": {"$meta": "textScore"}}, ).sort( [("score", {"$meta": "textScore"})] ).skip(skip).limit(limit) docs = await cursor.to_list(limit) items = await self._populate_categories_bulk(docs) return SearchResponse( items=items, total=total, query=query, page=page, limit=limit, )