o2o-negosium-original/lps/worker/handlers.py
민헌 fb72ab625f feat(lps): 코어 파이프라인 — mall 필터·IQR 이상치·top-N 최저가
검색 결과를 실제 최저가로 정제하는 파이프라인을 핸들러에 결합한다.
검색→필터→이상치→정렬→top-N. AI 유사도 판정 슬롯은 비워둠(키 대기).

- pipeline/filters: keep_only_mall·filter_out_malls·filter_by_price_band(요청 현재가 기준 targeted 컷)
- pipeline/outliers: IQR 기반 이상치 제거(z-score 대신 — 분포 가정 없음, stdlib만)
- pipeline/core: STAGE in/out 관측 로깅 + top-N 최저가(레퍼런스 pipeline_log 계승)
- handler: 검색→run_price_pipeline 결합, 요청 price 를 밴드 기준으로 사용
- tests: 정렬/IQR(극단 저·고가 제거)/밴드/mall/빈입력/STAGE/실 fixture 7건 → 전체 27/27
- 라이브 확인: 스탠리 텀블러 40건→top-5 최저가, STAGE 카운트 노출

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 17:06:15 +09:00

38 lines
1.5 KiB
Python

"""잡 핸들러 — job_type 별 처리. 현재는 SEARCH(검색)만.
검색 핸들러: 소스 어댑터로 검색 → 코어 파이프라인(필터·이상치·top-N 최저가) → 결과.
AI 유사도 판정은 파이프라인 슬롯에 키 준비 시 결합한다.
"""
from common.enums import JobType
from services.search.contract import SearchAdapter
from services.search.util import parse_price
from services.pipeline.core import run_price_pipeline
def build_search_handler(adapters: dict[str, SearchAdapter], default_source: str = "coupang", limit: int = 40, top_n: int = 5):
"""검색 핸들러 생성. adapters = {source: SearchAdapter}."""
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")
adapter = adapters.get(default_source)
if adapter is None:
raise ValueError(f"no adapter for source: {default_source}")
products = await adapter.search(query, limit=limit)
# 요청 현재가(있으면)를 가격 밴드 기준으로 사용 → 엉뚱한 저가/고가 targeted 컷
base_price = parse_price(payload.get("price"))
result = run_price_pipeline(products, base_price=base_price, top_n=top_n)
result["source"] = adapter.source
result["query"] = query
return result
return handler