"""잡 핸들러 — job_type 별 처리. 현재는 SEARCH(검색)만. 검색 핸들러: 여러 소스 어댑터를 동시 검색 → 병합 → 코어 파이프라인(필터·이상치·top-N 최저가). 소스별 실패는 격리한다(한 소스가 죽어도 나머지로 결과 산출). 모든 소스 실패 시에만 잡 실패(재시도). AI 유사도 판정은 파이프라인 슬롯에 키 준비 시 결합한다. """ import asyncio from common.enums import JobType from common.logger import LOG from services.search.contract import SearchAdapter from services.search.util import parse_price from services.pipeline.core import apply_filters, rank_result def build_search_handler( adapters: dict[str, SearchAdapter], sources: list[str] | None = None, limit: int = 40, top_n: int = 5, judge=None, ): """검색 핸들러 생성. adapters = {source: SearchAdapter}. sources 미지정 시 전체 사용. judge(SimilarityJudge) 주입 시 필터 뒤·top-N 앞에 '같은 상품' AI 판정을 끼운다(없으면 생략).""" use = list(sources) if sources else list(adapters.keys()) async def handler(job: dict) -> dict: if job["job_type"] != JobType.SEARCH.value: raise ValueError(f"unsupported job_type: {job['job_type']}") payload = job.get("payload") or {} query = (payload.get("product_name") or "").strip() if not query: raise ValueError("empty product_name") base_price = parse_price(payload.get("price")) # 소스 동시 검색 (실패는 예외로 수거해 격리) results = await asyncio.gather( *[adapters[s].search(query, limit=limit) for s in use], return_exceptions=True, ) products = [] per_source: dict[str, dict] = {} for src, res in zip(use, results): if isinstance(res, Exception): LOG.w(f"[{src}] 검색 실패: {type(res).__name__}: {res}") per_source[src] = {"error": f"{type(res).__name__}: {res}"} else: products.extend(res) per_source[src] = {"count": len(res)} if not products and all("error" in v for v in per_source.values()): raise RuntimeError(f"모든 소스 검색 실패: {per_source}") # 잡 실패 → 재시도 # 필터(mall·밴드·이상치) → [AI 유사도 판정] → top-N 최저가 candidates, stages = apply_filters(products, base_price=base_price) if judge is not None and candidates: target = {k: payload.get(k, "") for k in ("product_name", "model", "specification", "company")} verdicts = await judge.judge(target, candidates) matched = [c for c, v in zip(candidates, verdicts) if v.is_match] stages.append({"stage": "ai_match", "in": len(candidates), "out": len(matched)}) candidates = matched result = rank_result(candidates, len(products), stages, top_n) result["query"] = query result["sources"] = per_source # 소스별 건수/에러 (관측) return result return handler