검색 결과를 실제 최저가로 정제하는 파이프라인을 핸들러에 결합한다. 검색→필터→이상치→정렬→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>
67 lines
2.2 KiB
Python
67 lines
2.2 KiB
Python
"""최저가 코어 파이프라인.
|
|
|
|
검색 결과(NormalizedProduct[]) → 필터 → 이상치 제거 → 정렬 → top-N 최저가.
|
|
각 STAGE 의 in/out 건수를 기록해 관측성을 확보한다(레퍼런스 pipeline_log 패턴 계승).
|
|
|
|
AI 유사도 판정(같은 상품인지)은 키 준비 시 이 사이(이상치 제거 뒤, top-N 앞)에 끼운다 —
|
|
지금은 슬롯만 비워두고 규칙 매칭을 하지 않는다(고정 파서 취약성 회피).
|
|
"""
|
|
|
|
from services.search.contract import NormalizedProduct
|
|
from services.pipeline.filters import keep_only_mall, filter_out_malls, filter_by_price_band
|
|
from services.pipeline.outliers import remove_price_outliers
|
|
|
|
|
|
def run_price_pipeline(
|
|
products: list[NormalizedProduct],
|
|
*,
|
|
base_price: int | None = None,
|
|
keep_mall: str | None = None,
|
|
banned_malls=(),
|
|
band_tolerance: float = 0.7,
|
|
remove_outliers: bool = True,
|
|
top_n: int = 5,
|
|
) -> dict:
|
|
stages: list[dict] = []
|
|
cur = list(products)
|
|
|
|
def stage(name: str, before: list, after: list):
|
|
stages.append({"stage": name, "in": len(before), "out": len(after)})
|
|
|
|
# 1) mall 필터 (쿠팡 단독이면 사실상 no-op, 네이버 결합 시 오픈마켓 정리용)
|
|
if keep_mall:
|
|
before = cur
|
|
cur = keep_only_mall(cur, keep_mall)
|
|
stage("keep_mall", before, cur)
|
|
elif banned_malls:
|
|
before = cur
|
|
cur = filter_out_malls(cur, banned_malls)
|
|
stage("filter_out_malls", before, cur)
|
|
|
|
# 2) 가격 밴드 (요청 현재가 기준 targeted 컷)
|
|
if base_price:
|
|
before = cur
|
|
cur = filter_by_price_band(cur, base_price, band_tolerance)
|
|
stage("price_band", before, cur)
|
|
|
|
# 3) 이상치 제거 (IQR)
|
|
if remove_outliers:
|
|
before = cur
|
|
cur, _ = remove_price_outliers(cur)
|
|
stage("outlier", before, cur)
|
|
|
|
# (AI 유사도 판정 슬롯 — 키 준비 시 여기)
|
|
|
|
# 4) 최저가순 정렬 + top-N
|
|
ranked = sorted(cur, key=lambda p: p.price)
|
|
top = ranked[:top_n]
|
|
stage("top_n", ranked, top)
|
|
|
|
return {
|
|
"total_found": len(products),
|
|
"kept": len(ranked),
|
|
"lowest": top[0].model_dump() if top else None,
|
|
"top": [p.model_dump() for p in top],
|
|
"stages": stages,
|
|
}
|