|
@@ -24,8 +24,9 @@ queryText 选择优先级(选第一个非空且非占位符 "-"):
|
|
|
|
|
|
|
|
import logging
|
|
import logging
|
|
|
import os
|
|
import os
|
|
|
|
|
+from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
from dataclasses import dataclass, field
|
|
from dataclasses import dataclass, field
|
|
|
-from typing import List, Optional
|
|
|
|
|
|
|
+from typing import Iterable, List, Optional
|
|
|
|
|
|
|
|
import httpx
|
|
import httpx
|
|
|
|
|
|
|
@@ -80,6 +81,12 @@ SUBSTANCE_TO_VIDEO_CONFIG = {
|
|
|
"KEYPOINT_SUBSTANCE": "VIDEO_KEYPOINT",
|
|
"KEYPOINT_SUBSTANCE": "VIDEO_KEYPOINT",
|
|
|
"PURPOSE_SUBSTANCE": "VIDEO_PURPOSE",
|
|
"PURPOSE_SUBSTANCE": "VIDEO_PURPOSE",
|
|
|
}
|
|
}
|
|
|
|
|
+ODPS_FEATURE_TO_CONFIG = {
|
|
|
|
|
+ ("解构选题", ""): "VIDEO_TOPIC",
|
|
|
|
|
+ ("实质", "灵感点"): "INSPIRATION_SUBSTANCE",
|
|
|
|
|
+ ("实质", "关键点"): "KEYPOINT_SUBSTANCE",
|
|
|
|
|
+ ("实质", "目的点"): "PURPOSE_SUBSTANCE",
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
# fallback(动态接口失败时用)
|
|
# fallback(动态接口失败时用)
|
|
|
MATERIAL_EFFECTIVE_CONFIG_CODES = ["VIDEO_TOPIC"] + list(POINT_TYPE_TO_SUBSTANCE.values())
|
|
MATERIAL_EFFECTIVE_CONFIG_CODES = ["VIDEO_TOPIC"] + list(POINT_TYPE_TO_SUBSTANCE.values())
|
|
@@ -100,6 +107,37 @@ EXCLUDED_COVER_URL_PATTERNS = (
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+@dataclass
|
|
|
|
|
+class RecallQuery:
|
|
|
|
|
+ """One ODPS-derived material recall query."""
|
|
|
|
|
+
|
|
|
|
|
+ query_text: str
|
|
|
|
|
+ config_code: str
|
|
|
|
|
+ element_dimension: str
|
|
|
|
|
+ point_type: str
|
|
|
|
|
+ standard_element: str
|
|
|
|
|
+ contribution_score: float = 0.0
|
|
|
|
|
+ dt: str = ""
|
|
|
|
|
+
|
|
|
|
|
+ @property
|
|
|
|
|
+ def strategy_name(self) -> str:
|
|
|
|
|
+ if self.element_dimension == "解构选题":
|
|
|
|
|
+ return "解构选题"
|
|
|
|
|
+ return f"{self.element_dimension}-{self.point_type}"
|
|
|
|
|
+
|
|
|
|
|
+ def to_hit(self) -> dict:
|
|
|
|
|
+ return {
|
|
|
|
|
+ "strategy": self.strategy_name,
|
|
|
|
|
+ "element_dimension": self.element_dimension,
|
|
|
|
|
+ "point_type": self.point_type,
|
|
|
|
|
+ "standard_element": self.standard_element,
|
|
|
|
|
+ "query_text": self.query_text,
|
|
|
|
|
+ "config_code": self.config_code,
|
|
|
|
|
+ "contribution_score": self.contribution_score,
|
|
|
|
|
+ "dt": self.dt,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
@dataclass
|
|
@dataclass
|
|
|
class Material:
|
|
class Material:
|
|
|
"""召回的创意素材。
|
|
"""召回的创意素材。
|
|
@@ -119,6 +157,13 @@ class Material:
|
|
|
roi: Optional[float] = None # ROI(收入/成本)
|
|
roi: Optional[float] = None # ROI(收入/成本)
|
|
|
impressions: Optional[int] = None # 累计曝光
|
|
impressions: Optional[int] = None # 累计曝光
|
|
|
quality_score: Optional[float] = None # 服务端 qualityScore
|
|
quality_score: Optional[float] = None # 服务端 qualityScore
|
|
|
|
|
+ recall_strategy: str = ""
|
|
|
|
|
+ recall_query_text: str = ""
|
|
|
|
|
+ recall_config_code: str = ""
|
|
|
|
|
+ recall_element_dimension: str = ""
|
|
|
|
|
+ recall_point_type: str = ""
|
|
|
|
|
+ recall_standard_element: str = ""
|
|
|
|
|
+ recall_hit_queries: List[dict] = field(default_factory=list)
|
|
|
# 原始 item dict(以备后用)
|
|
# 原始 item dict(以备后用)
|
|
|
raw: dict = field(default_factory=dict, repr=False)
|
|
raw: dict = field(default_factory=dict, repr=False)
|
|
|
|
|
|
|
@@ -295,21 +340,42 @@ def _call_batch_by_text(
|
|
|
return payload.get("items") or []
|
|
return payload.get("items") or []
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _items_to_materials(items: List[dict], min_impressions: int, min_ctr: float) -> tuple:
|
|
|
|
|
- """把 batchByText items 过滤 + 转 Material(2026-06-10 复用 helper)。
|
|
|
|
|
|
|
+def _as_float(value, default: float = 0.0) -> float:
|
|
|
|
|
+ try:
|
|
|
|
|
+ if value is None:
|
|
|
|
|
+ return default
|
|
|
|
|
+ return float(value)
|
|
|
|
|
+ except (TypeError, ValueError):
|
|
|
|
|
+ return default
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _as_int(value, default: int = 0) -> int:
|
|
|
|
|
+ try:
|
|
|
|
|
+ if value is None:
|
|
|
|
|
+ return default
|
|
|
|
|
+ return int(float(value))
|
|
|
|
|
+ except (TypeError, ValueError):
|
|
|
|
|
+ return default
|
|
|
|
|
+
|
|
|
|
|
|
|
|
- 过滤条件(全部满足才保留):
|
|
|
|
|
|
|
+def _items_to_materials(items: List[dict], sim_threshold: float) -> tuple:
|
|
|
|
|
+ """把召回 items 过滤 + 转 Material。
|
|
|
|
|
+
|
|
|
|
|
+ 过滤条件:
|
|
|
- modality=MATERIAL(防御性)
|
|
- modality=MATERIAL(防御性)
|
|
|
- cover URL 不在黑名单
|
|
- cover URL 不在黑名单
|
|
|
- - impressions > min_impressions
|
|
|
|
|
- - ctr >= min_ctr(2026-06-10 加,5% 太低)
|
|
|
|
|
|
|
+ - score >= sim_threshold
|
|
|
|
|
|
|
|
- 返回 (materials, blacklist_n, low_imp_n, low_ctr_n)。
|
|
|
|
|
|
|
+ CTR / impressions 只作为审批展示和兜底排序参考,不再作为硬筛。
|
|
|
|
|
+ 返回 (materials, stats)。
|
|
|
"""
|
|
"""
|
|
|
out: List[Material] = []
|
|
out: List[Material] = []
|
|
|
- excluded_blacklist = 0
|
|
|
|
|
- excluded_low_imp = 0
|
|
|
|
|
- excluded_low_ctr = 0
|
|
|
|
|
|
|
+ stats = {
|
|
|
|
|
+ "blacklist": 0,
|
|
|
|
|
+ "low_score": 0,
|
|
|
|
|
+ "low_imp": 0,
|
|
|
|
|
+ "low_ctr": 0,
|
|
|
|
|
+ }
|
|
|
for it in items:
|
|
for it in items:
|
|
|
if it.get("modality") != "MATERIAL":
|
|
if it.get("modality") != "MATERIAL":
|
|
|
continue
|
|
continue
|
|
@@ -318,119 +384,273 @@ def _items_to_materials(items: List[dict], min_impressions: int, min_ctr: float)
|
|
|
continue
|
|
continue
|
|
|
cover = it.get("cover") or ""
|
|
cover = it.get("cover") or ""
|
|
|
if any(p in cover for p in EXCLUDED_COVER_URL_PATTERNS):
|
|
if any(p in cover for p in EXCLUDED_COVER_URL_PATTERNS):
|
|
|
- excluded_blacklist += 1
|
|
|
|
|
|
|
+ stats["blacklist"] += 1
|
|
|
|
|
+ continue
|
|
|
|
|
+ score = _as_float(it.get("score"))
|
|
|
|
|
+ if score < sim_threshold:
|
|
|
|
|
+ stats["low_score"] += 1
|
|
|
continue
|
|
continue
|
|
|
md = it.get("materialDetail") or {}
|
|
md = it.get("materialDetail") or {}
|
|
|
q = md.get("quality") or {}
|
|
q = md.get("quality") or {}
|
|
|
- imp = q.get("impressions") or 0
|
|
|
|
|
- if imp <= min_impressions:
|
|
|
|
|
- excluded_low_imp += 1
|
|
|
|
|
- continue
|
|
|
|
|
- ctr = q.get("ctr") or 0.0
|
|
|
|
|
- if ctr < min_ctr:
|
|
|
|
|
- excluded_low_ctr += 1
|
|
|
|
|
- continue
|
|
|
|
|
out.append(Material(
|
|
out.append(Material(
|
|
|
material_id=str(mid),
|
|
material_id=str(mid),
|
|
|
- score=float(it.get("score") or 0.0),
|
|
|
|
|
|
|
+ score=score,
|
|
|
title=it.get("title") or "",
|
|
title=it.get("title") or "",
|
|
|
cover=cover,
|
|
cover=cover,
|
|
|
video_url=it.get("videoUrl") or "",
|
|
video_url=it.get("videoUrl") or "",
|
|
|
- cost=q.get("cost"),
|
|
|
|
|
- ctr=q.get("ctr"),
|
|
|
|
|
- cvr=q.get("cvr"),
|
|
|
|
|
- roi=q.get("roi"),
|
|
|
|
|
- impressions=imp,
|
|
|
|
|
- quality_score=q.get("qualityScore"),
|
|
|
|
|
|
|
+ cost=_as_float(q.get("cost")) if q.get("cost") is not None else None,
|
|
|
|
|
+ ctr=_as_float(q.get("ctr")) if q.get("ctr") is not None else None,
|
|
|
|
|
+ cvr=_as_float(q.get("cvr")) if q.get("cvr") is not None else None,
|
|
|
|
|
+ roi=_as_float(q.get("roi")) if q.get("roi") is not None else None,
|
|
|
|
|
+ impressions=_as_int(q.get("impressions")) if q.get("impressions") is not None else None,
|
|
|
|
|
+ quality_score=_as_float(q.get("qualityScore")) if q.get("qualityScore") is not None else None,
|
|
|
raw=it,
|
|
raw=it,
|
|
|
))
|
|
))
|
|
|
- return out, excluded_blacklist, excluded_low_imp, excluded_low_ctr
|
|
|
|
|
|
|
+ return out, stats
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _sort_materials_by_policy(materials: List[Material]) -> List[Material]:
|
|
|
|
|
+ """生产排序策略:先相关性准入,再按历史消耗倒序。"""
|
|
|
|
|
+ return sorted(
|
|
|
|
|
+ materials,
|
|
|
|
|
+ key=lambda m: (
|
|
|
|
|
+ m.cost is not None,
|
|
|
|
|
+ m.cost or 0,
|
|
|
|
|
+ m.roi or 0,
|
|
|
|
|
+ m.impressions or 0,
|
|
|
|
|
+ m.ctr or 0,
|
|
|
|
|
+ m.quality_score or 0,
|
|
|
|
|
+ m.score or 0,
|
|
|
|
|
+ ),
|
|
|
|
|
+ reverse=True,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _material_rank(material: Material) -> tuple:
|
|
|
|
|
+ return (
|
|
|
|
|
+ material.cost is not None,
|
|
|
|
|
+ material.cost or 0,
|
|
|
|
|
+ material.roi or 0,
|
|
|
|
|
+ material.impressions or 0,
|
|
|
|
|
+ material.ctr or 0,
|
|
|
|
|
+ material.quality_score or 0,
|
|
|
|
|
+ material.score or 0,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _feature_attr(feature, name: str, default=""):
|
|
|
|
|
+ if isinstance(feature, dict):
|
|
|
|
|
+ return feature.get(name, default)
|
|
|
|
|
+ return getattr(feature, name, default)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _build_recall_queries_from_features(
|
|
|
|
|
+ element_features: Iterable,
|
|
|
|
|
+ query_limit: int,
|
|
|
|
|
+) -> List[RecallQuery]:
|
|
|
|
|
+ """Build recall queries from ODPS features.
|
|
|
|
|
+
|
|
|
|
|
+ Supported dimensions:
|
|
|
|
|
+ - 解构选题 -> VIDEO_TOPIC
|
|
|
|
|
+ - 实质 + 灵感点/关键点/目的点 -> *_SUBSTANCE
|
|
|
|
|
+ """
|
|
|
|
|
+ queries: List[RecallQuery] = []
|
|
|
|
|
+ seen = set()
|
|
|
|
|
+ raw_features = list(element_features or [])
|
|
|
|
|
+ sorted_features = sorted(
|
|
|
|
|
+ raw_features,
|
|
|
|
|
+ key=lambda f: (
|
|
|
|
|
+ 0 if str(_feature_attr(f, "element_dimension") or "") == "解构选题" else 1,
|
|
|
|
|
+ -float(_feature_attr(f, "contribution_score", 0) or 0),
|
|
|
|
|
+ ),
|
|
|
|
|
+ )
|
|
|
|
|
+ for feature in sorted_features:
|
|
|
|
|
+ element_dimension = str(_feature_attr(feature, "element_dimension") or "").strip()
|
|
|
|
|
+ point_type = str(_feature_attr(feature, "point_type") or "").strip()
|
|
|
|
|
+ standard_element = str(_feature_attr(feature, "standard_element") or "").strip()
|
|
|
|
|
+ if standard_element in PLACEHOLDER_VALUES:
|
|
|
|
|
+ continue
|
|
|
|
|
+ config_code = ODPS_FEATURE_TO_CONFIG.get((element_dimension, point_type))
|
|
|
|
|
+ if not config_code and element_dimension == "解构选题":
|
|
|
|
|
+ config_code = ODPS_FEATURE_TO_CONFIG.get(("解构选题", ""))
|
|
|
|
|
+ if not config_code:
|
|
|
|
|
+ continue
|
|
|
|
|
+ key = (standard_element, config_code)
|
|
|
|
|
+ if key in seen:
|
|
|
|
|
+ continue
|
|
|
|
|
+ seen.add(key)
|
|
|
|
|
+ queries.append(RecallQuery(
|
|
|
|
|
+ query_text=standard_element,
|
|
|
|
|
+ config_code=config_code,
|
|
|
|
|
+ element_dimension=element_dimension,
|
|
|
|
|
+ point_type=point_type,
|
|
|
|
|
+ standard_element=standard_element,
|
|
|
|
|
+ contribution_score=float(_feature_attr(feature, "contribution_score", 0) or 0),
|
|
|
|
|
+ dt=str(_feature_attr(feature, "dt") or ""),
|
|
|
|
|
+ ))
|
|
|
|
|
+ if len(queries) >= query_limit:
|
|
|
|
|
+ break
|
|
|
|
|
+ return queries
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _call_batch_for_recall_query(
|
|
|
|
|
+ query: RecallQuery,
|
|
|
|
|
+ *,
|
|
|
|
|
+ display_k: int,
|
|
|
|
|
+ days: int,
|
|
|
|
|
+ sim_threshold: float,
|
|
|
|
|
+ alpha: float,
|
|
|
|
|
+ w_ctr: float,
|
|
|
|
|
+ w_cvr: float,
|
|
|
|
|
+ w_roi: float,
|
|
|
|
|
+ w_open_rate: float,
|
|
|
|
|
+ w_fission_rate: float,
|
|
|
|
|
+ deconstruct_boost: float,
|
|
|
|
|
+ source_labels: List[str],
|
|
|
|
|
+) -> tuple[RecallQuery, List[dict]]:
|
|
|
|
|
+ items = _call_batch_by_text(
|
|
|
|
|
+ query_text=query.query_text,
|
|
|
|
|
+ config_codes=[query.config_code],
|
|
|
|
|
+ display_k=display_k,
|
|
|
|
|
+ days=days,
|
|
|
|
|
+ sim_threshold=sim_threshold,
|
|
|
|
|
+ alpha=alpha,
|
|
|
|
|
+ w_ctr=w_ctr,
|
|
|
|
|
+ w_cvr=w_cvr,
|
|
|
|
|
+ w_roi=w_roi,
|
|
|
|
|
+ w_open_rate=w_open_rate,
|
|
|
|
|
+ w_fission_rate=w_fission_rate,
|
|
|
|
|
+ deconstruct_boost=deconstruct_boost,
|
|
|
|
|
+ source_labels=source_labels,
|
|
|
|
|
+ modalities=["MATERIAL"],
|
|
|
|
|
+ )
|
|
|
|
|
+ return query, items
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _merge_materials_by_policy(query_materials: List[tuple[RecallQuery, List[Material]]]) -> List[Material]:
|
|
|
|
|
+ by_mid: dict[str, Material] = {}
|
|
|
|
|
+ for query, materials in query_materials:
|
|
|
|
|
+ hit = query.to_hit()
|
|
|
|
|
+ for material in materials:
|
|
|
|
|
+ material.recall_hit_queries = [hit]
|
|
|
|
|
+ material.recall_strategy = query.strategy_name
|
|
|
|
|
+ material.recall_query_text = query.query_text
|
|
|
|
|
+ material.recall_config_code = query.config_code
|
|
|
|
|
+ material.recall_element_dimension = query.element_dimension
|
|
|
|
|
+ material.recall_point_type = query.point_type
|
|
|
|
|
+ material.recall_standard_element = query.standard_element
|
|
|
|
|
+ existing = by_mid.get(material.material_id)
|
|
|
|
|
+ if existing is None:
|
|
|
|
|
+ by_mid[material.material_id] = material
|
|
|
|
|
+ continue
|
|
|
|
|
+ merged_hits = existing.recall_hit_queries + [
|
|
|
|
|
+ h for h in material.recall_hit_queries
|
|
|
|
|
+ if h not in existing.recall_hit_queries
|
|
|
|
|
+ ]
|
|
|
|
|
+ if _material_rank(material) > _material_rank(existing):
|
|
|
|
|
+ material.recall_hit_queries = merged_hits
|
|
|
|
|
+ by_mid[material.material_id] = material
|
|
|
|
|
+ else:
|
|
|
|
|
+ existing.recall_hit_queries = merged_hits
|
|
|
|
|
+ return _sort_materials_by_policy(list(by_mid.values()))
|
|
|
|
|
|
|
|
|
|
|
|
|
def recall_materials_for_video(
|
|
def recall_materials_for_video(
|
|
|
landing: LandingVideo,
|
|
landing: LandingVideo,
|
|
|
final_top_n: int = DEFAULT_FINAL_TOP_N,
|
|
final_top_n: int = DEFAULT_FINAL_TOP_N,
|
|
|
source_labels: Optional[List[str]] = None,
|
|
source_labels: Optional[List[str]] = None,
|
|
|
|
|
+ element_features: Optional[Iterable] = None,
|
|
|
) -> List[Material]:
|
|
) -> List[Material]:
|
|
|
- """素材召回(2026-06-10 用户最终:2 维度 strategy + fallback,batchByText 单 configCode)。
|
|
|
|
|
|
|
+ """素材召回:用 ODPS 多维特征并行召回并合并排序。
|
|
|
|
|
|
|
|
流程:
|
|
流程:
|
|
|
- 1. _pick_query_strategies_for_batch(landing) 返回 1~2 个策略候选
|
|
|
|
|
- 维度 1: (standard_element, *_SUBSTANCE 按 point_type) — 优先
|
|
|
|
|
- 维度 2: (demand_content_topic, VIDEO_TOPIC)
|
|
|
|
|
- 2. 对每个策略调一次 batchByText(只传 1 个 configCode)
|
|
|
|
|
- 首次返回 ≥ 1 条 impressions>阈值 的 material → break,直接返回
|
|
|
|
|
- 3. 全部策略都失败 → 返回空 → 上层 prepare 换 landing
|
|
|
|
|
-
|
|
|
|
|
- 阈值不变保证质量(用户 2026-06-10 确认):依赖"多次尝试"提升命中率。
|
|
|
|
|
|
|
+ 1. 从 ODPS features 生成 query:解构选题 + 实质三点。
|
|
|
|
|
+ 2. 多 query 并行调用 batchByText。
|
|
|
|
|
+ 3. 汇总、material_id 去重、score>=阈值、按 cost 倒序。
|
|
|
|
|
+
|
|
|
|
|
+ 当前硬筛只保留相似度阈值;曝光/CTR 进入审批表但不拦截。
|
|
|
"""
|
|
"""
|
|
|
from config import (
|
|
from config import (
|
|
|
RECALL_ALPHA, RECALL_DAYS, RECALL_DECONSTRUCT_BOOST,
|
|
RECALL_ALPHA, RECALL_DAYS, RECALL_DECONSTRUCT_BOOST,
|
|
|
- RECALL_DISPLAY_K, RECALL_MIN_CTR, RECALL_MIN_IMPRESSIONS,
|
|
|
|
|
|
|
+ RECALL_DISPLAY_K, RECALL_PARALLEL_MAX_WORKERS, RECALL_QUERY_LIMIT_PER_VIDEO,
|
|
|
RECALL_SIM_THRESHOLD, RECALL_SOURCE_LABELS,
|
|
RECALL_SIM_THRESHOLD, RECALL_SOURCE_LABELS,
|
|
|
RECALL_W_CTR, RECALL_W_CVR, RECALL_W_FISSION_RATE,
|
|
RECALL_W_CTR, RECALL_W_CVR, RECALL_W_FISSION_RATE,
|
|
|
RECALL_W_OPEN_RATE, RECALL_W_ROI,
|
|
RECALL_W_OPEN_RATE, RECALL_W_ROI,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- strategies = _pick_query_strategies_for_batch(landing)
|
|
|
|
|
- if not strategies:
|
|
|
|
|
|
|
+ if element_features is None:
|
|
|
|
|
+ element_features = landing.raw.get("element_features") or []
|
|
|
|
|
+ queries = _build_recall_queries_from_features(
|
|
|
|
|
+ element_features,
|
|
|
|
|
+ query_limit=max(1, RECALL_QUERY_LIMIT_PER_VIDEO),
|
|
|
|
|
+ )
|
|
|
|
|
+ if not queries:
|
|
|
logger.warning(
|
|
logger.warning(
|
|
|
- "[material_recall] landing video_id=%d 无可用策略(2 维度都缺),返回空",
|
|
|
|
|
|
|
+ "[material_recall] landing video_id=%d 无 ODPS 可用召回特征,返回空",
|
|
|
landing.video_id,
|
|
landing.video_id,
|
|
|
)
|
|
)
|
|
|
return []
|
|
return []
|
|
|
|
|
|
|
|
logger.info(
|
|
logger.info(
|
|
|
- "[material_recall] landing video_id=%d 走 %d 个策略",
|
|
|
|
|
- landing.video_id, len(strategies),
|
|
|
|
|
|
|
+ "[material_recall] landing video_id=%d 走 %d 个 ODPS 策略:%s",
|
|
|
|
|
+ landing.video_id, len(queries),
|
|
|
|
|
+ "; ".join(f"{q.strategy_name}:{q.query_text}->{q.config_code}" for q in queries),
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- for query_text, config_code, name in strategies:
|
|
|
|
|
- logger.info(
|
|
|
|
|
- "[material_recall] 策略=%s q=%r configCode=%s",
|
|
|
|
|
- name, query_text[:30], config_code,
|
|
|
|
|
- )
|
|
|
|
|
- try:
|
|
|
|
|
- items = _call_batch_by_text(
|
|
|
|
|
- query_text=query_text,
|
|
|
|
|
- config_codes=[config_code], # ← 只传当前策略的 1 个
|
|
|
|
|
|
|
+ query_materials: List[tuple[RecallQuery, List[Material]]] = []
|
|
|
|
|
+ labels = source_labels or RECALL_SOURCE_LABELS
|
|
|
|
|
+ max_workers = max(1, min(RECALL_PARALLEL_MAX_WORKERS, len(queries)))
|
|
|
|
|
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
|
|
|
+ futures = [
|
|
|
|
|
+ executor.submit(
|
|
|
|
|
+ _call_batch_for_recall_query,
|
|
|
|
|
+ query,
|
|
|
display_k=RECALL_DISPLAY_K,
|
|
display_k=RECALL_DISPLAY_K,
|
|
|
days=RECALL_DAYS,
|
|
days=RECALL_DAYS,
|
|
|
sim_threshold=RECALL_SIM_THRESHOLD,
|
|
sim_threshold=RECALL_SIM_THRESHOLD,
|
|
|
alpha=RECALL_ALPHA,
|
|
alpha=RECALL_ALPHA,
|
|
|
- w_ctr=RECALL_W_CTR, w_cvr=RECALL_W_CVR, w_roi=RECALL_W_ROI,
|
|
|
|
|
- w_open_rate=RECALL_W_OPEN_RATE, w_fission_rate=RECALL_W_FISSION_RATE,
|
|
|
|
|
|
|
+ w_ctr=RECALL_W_CTR,
|
|
|
|
|
+ w_cvr=RECALL_W_CVR,
|
|
|
|
|
+ w_roi=RECALL_W_ROI,
|
|
|
|
|
+ w_open_rate=RECALL_W_OPEN_RATE,
|
|
|
|
|
+ w_fission_rate=RECALL_W_FISSION_RATE,
|
|
|
deconstruct_boost=RECALL_DECONSTRUCT_BOOST,
|
|
deconstruct_boost=RECALL_DECONSTRUCT_BOOST,
|
|
|
- source_labels=source_labels or RECALL_SOURCE_LABELS,
|
|
|
|
|
- modalities=["MATERIAL"],
|
|
|
|
|
|
|
+ source_labels=labels,
|
|
|
)
|
|
)
|
|
|
- except Exception as e:
|
|
|
|
|
- logger.error(
|
|
|
|
|
- "[material_recall] 策略 %s 失败,试下一个:%s", name, e,
|
|
|
|
|
|
|
+ for query in queries
|
|
|
|
|
+ ]
|
|
|
|
|
+ for future in as_completed(futures):
|
|
|
|
|
+ try:
|
|
|
|
|
+ query, items = future.result()
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error("[material_recall] 并行 batchByText 失败:%s", e)
|
|
|
|
|
+ continue
|
|
|
|
|
+ mats, stats = _items_to_materials(items, RECALL_SIM_THRESHOLD)
|
|
|
|
|
+ query_materials.append((query, mats))
|
|
|
|
|
+ logger.info(
|
|
|
|
|
+ "[material_recall] 策略=%s q=%r configCode=%s 返回 %d 条,⊘ 黑名单 %d,⊘ score<%.2f %d → 保留 %d",
|
|
|
|
|
+ query.strategy_name, query.query_text[:30], query.config_code,
|
|
|
|
|
+ len(items), stats["blacklist"], RECALL_SIM_THRESHOLD,
|
|
|
|
|
+ stats["low_score"], len(mats),
|
|
|
)
|
|
)
|
|
|
- continue
|
|
|
|
|
|
|
|
|
|
- mats, n_bl, n_low_imp, n_low_ctr = _items_to_materials(
|
|
|
|
|
- items, RECALL_MIN_IMPRESSIONS, RECALL_MIN_CTR,
|
|
|
|
|
- )
|
|
|
|
|
- logger.info(
|
|
|
|
|
- "[material_recall] 服务端返回 %d 条,⊘ 黑名单 %d,⊘ imp<=%d %d,⊘ ctr<%.2f %d → 保留 %d",
|
|
|
|
|
- len(items), n_bl,
|
|
|
|
|
- RECALL_MIN_IMPRESSIONS, n_low_imp,
|
|
|
|
|
- RECALL_MIN_CTR, n_low_ctr,
|
|
|
|
|
- len(mats),
|
|
|
|
|
- )
|
|
|
|
|
- if mats:
|
|
|
|
|
- return mats[:final_top_n]
|
|
|
|
|
|
|
+ merged = _merge_materials_by_policy(query_materials)
|
|
|
|
|
+ logger.info(
|
|
|
|
|
+ "[material_recall] landing video_id=%d 多维召回合并后保留 %d 条(cost desc)",
|
|
|
|
|
+ landing.video_id, len(merged),
|
|
|
|
|
+ )
|
|
|
|
|
+ if merged:
|
|
|
|
|
+ return merged[:final_top_n]
|
|
|
|
|
|
|
|
|
|
+ for query in queries:
|
|
|
# batchByText 当前可能对 MATERIAL 返回 0;降级到历史 matchByText 路径。
|
|
# batchByText 当前可能对 MATERIAL 返回 0;降级到历史 matchByText 路径。
|
|
|
- for fallback_cc in _fallback_config_codes_for_strategy(config_code, landing):
|
|
|
|
|
|
|
+ for fallback_cc in _fallback_config_codes_for_strategy(query.config_code, landing):
|
|
|
try:
|
|
try:
|
|
|
fallback_items = _call_match_by_text(
|
|
fallback_items = _call_match_by_text(
|
|
|
- query_text=query_text,
|
|
|
|
|
|
|
+ query_text=query.query_text,
|
|
|
config_code=fallback_cc,
|
|
config_code=fallback_cc,
|
|
|
material_top_n=RECALL_DISPLAY_K,
|
|
material_top_n=RECALL_DISPLAY_K,
|
|
|
- source_labels=source_labels or RECALL_SOURCE_LABELS,
|
|
|
|
|
|
|
+ source_labels=labels,
|
|
|
)
|
|
)
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
logger.error(
|
|
logger.error(
|
|
@@ -439,34 +659,28 @@ def recall_materials_for_video(
|
|
|
)
|
|
)
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
|
- fmats, fn_bl, fn_low_imp, fn_low_ctr = _items_to_materials(
|
|
|
|
|
- fallback_items, RECALL_MIN_IMPRESSIONS, RECALL_MIN_CTR,
|
|
|
|
|
|
|
+ fmats, fstats = _items_to_materials(fallback_items, RECALL_SIM_THRESHOLD)
|
|
|
|
|
+ fquery = RecallQuery(
|
|
|
|
|
+ query_text=query.query_text,
|
|
|
|
|
+ config_code=fallback_cc,
|
|
|
|
|
+ element_dimension=query.element_dimension,
|
|
|
|
|
+ point_type=query.point_type,
|
|
|
|
|
+ standard_element=query.standard_element,
|
|
|
|
|
+ contribution_score=query.contribution_score,
|
|
|
|
|
+ dt=query.dt,
|
|
|
)
|
|
)
|
|
|
- fn_low_score = sum(1 for m in fmats if (m.score or 0.0) < RECALL_SIM_THRESHOLD)
|
|
|
|
|
- fmats = [m for m in fmats if (m.score or 0.0) >= RECALL_SIM_THRESHOLD]
|
|
|
|
|
|
|
+ fmats = _merge_materials_by_policy([(fquery, fmats)])
|
|
|
logger.info(
|
|
logger.info(
|
|
|
- "[material_recall] fallback=%s 返回 %d 条,⊘ 黑名单 %d,⊘ imp<=%d %d,⊘ ctr<%.2f %d,⊘ score<%.2f %d → 保留 %d",
|
|
|
|
|
- fallback_cc, len(fallback_items), fn_bl,
|
|
|
|
|
- RECALL_MIN_IMPRESSIONS, fn_low_imp,
|
|
|
|
|
- RECALL_MIN_CTR, fn_low_ctr,
|
|
|
|
|
- RECALL_SIM_THRESHOLD, fn_low_score,
|
|
|
|
|
|
|
+ "[material_recall] fallback=%s 返回 %d 条,⊘ 黑名单 %d,⊘ score<%.2f %d → 保留 %d(cost desc)",
|
|
|
|
|
+ fallback_cc, len(fallback_items), fstats["blacklist"],
|
|
|
|
|
+ RECALL_SIM_THRESHOLD, fstats["low_score"],
|
|
|
len(fmats),
|
|
len(fmats),
|
|
|
)
|
|
)
|
|
|
if fmats:
|
|
if fmats:
|
|
|
- fmats.sort(
|
|
|
|
|
- key=lambda m: (
|
|
|
|
|
- m.ctr or 0,
|
|
|
|
|
- m.impressions or 0,
|
|
|
|
|
- m.quality_score or 0,
|
|
|
|
|
- m.score or 0,
|
|
|
|
|
- ),
|
|
|
|
|
- reverse=True,
|
|
|
|
|
- )
|
|
|
|
|
return fmats[:final_top_n]
|
|
return fmats[:final_top_n]
|
|
|
- # else 继续下一个策略
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
logger.info(
|
|
|
- "[material_recall] landing video_id=%d 所有 %d 策略全失败,返回空(上层换 landing)",
|
|
|
|
|
- landing.video_id, len(strategies),
|
|
|
|
|
|
|
+ "[material_recall] landing video_id=%d 所有 %d ODPS 策略全失败,返回空(上层换 landing)",
|
|
|
|
|
+ landing.video_id, len(queries),
|
|
|
)
|
|
)
|
|
|
return []
|
|
return []
|