belong_rel.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. """
  2. 补充 demand_belong_category 与 multi_demand_pool_di 的匹配关系,
  3. 并回填词级 video_list(匹配池视频去重截断最多 10 个)。
  4. """
  5. from __future__ import annotations
  6. import json
  7. import logging
  8. from typing import Any
  9. from supply_infra.db.repositories.demand_belong_category_repo import (
  10. DemandBelongCategoryRepository,
  11. )
  12. from supply_infra.db.repositories.demand_belong_pool_rel_repo import (
  13. DemandBelongPoolRelRepository,
  14. )
  15. from supply_infra.db.repositories.multi_demand_pool_di_repo import (
  16. MultiDemandPoolDiRepository,
  17. )
  18. from supply_infra.db.session import get_session
  19. logger = logging.getLogger(__name__)
  20. _VIDEO_LIST_LIMIT = 10
  21. def _parse_video_ids(raw: str | None) -> list[str]:
  22. if not raw:
  23. return []
  24. try:
  25. parsed = json.loads(raw)
  26. except json.JSONDecodeError:
  27. return []
  28. if not isinstance(parsed, list):
  29. return []
  30. return [str(v).strip() for v in parsed if v is not None and str(v).strip()]
  31. def _merge_video_ids(video_lists: list[str | None], limit: int = _VIDEO_LIST_LIMIT) -> str | None:
  32. """按出现顺序去重,截断到 limit。"""
  33. seen: set[str] = set()
  34. ordered: list[str] = []
  35. for raw in video_lists:
  36. for vid in _parse_video_ids(raw):
  37. if vid in seen:
  38. continue
  39. seen.add(vid)
  40. ordered.append(vid)
  41. if len(ordered) >= limit:
  42. return json.dumps(ordered, ensure_ascii=False)
  43. if not ordered:
  44. return None
  45. return json.dumps(ordered, ensure_ascii=False)
  46. def _matches_explicit_token(name: str, demand_name: str) -> bool:
  47. """只接受上游空格分词的显式词,不把任意子串直接升级为正式关系。"""
  48. normalized_name = name.strip()
  49. normalized_demand = demand_name.strip()
  50. if not normalized_name or not normalized_demand:
  51. return False
  52. return normalized_name in {
  53. token.strip() for token in normalized_demand.split() if token.strip()
  54. }
  55. def sync_demand_belong_pool_rel(biz_dt: str) -> dict[str, Any]:
  56. """
  57. 增量建立词 ↔ 需求池匹配边,并更新词级 video_list。
  58. - 只读取 biz_dt 当日需求池,禁止跨日串数
  59. - name 必须是 demand_name 的显式空格分词,不接受任意子串
  60. - 当日关系先清理再精确重建,源内容修订后不会保留陈旧边
  61. - video_list:匹配行视频按顺序去重,最多 10 个
  62. """
  63. if len(str(biz_dt)) != 8 or not str(biz_dt).isdigit():
  64. raise ValueError(f"biz_dt 格式无效,应为 YYYYMMDD: {biz_dt!r}")
  65. logger.info("Starting demand_belong_pool_rel sync: biz_dt=%s", biz_dt)
  66. with get_session() as session:
  67. words = DemandBelongCategoryRepository(session).list_active_id_name()
  68. pool_rows = MultiDemandPoolDiRepository(session).list_id_name_video_lists(biz_dt)
  69. logger.info("Loaded words=%d pool_rows=%d", len(words), len(pool_rows))
  70. if not words or not pool_rows:
  71. result = {
  72. "words": len(words),
  73. "pool_rows": len(pool_rows),
  74. "biz_dt": biz_dt,
  75. "matched_edges": 0,
  76. "rejected_substring_candidates": 0,
  77. "replaced_edges": 0,
  78. "inserted": 0,
  79. "video_updated": 0,
  80. }
  81. logger.info("Nothing to sync: %s", result)
  82. return result
  83. candidate_pairs: list[tuple[int, int]] = []
  84. video_updates: dict[int, str | None] = {}
  85. matched_edges = 0
  86. rejected_substring_candidates = 0
  87. for belong_id, name in words:
  88. matched_pool_ids: list[int] = []
  89. matched_video_lists: list[str | None] = []
  90. for pool_id, demand_name, video_list in pool_rows:
  91. if _matches_explicit_token(name, demand_name):
  92. matched_pool_ids.append(pool_id)
  93. matched_video_lists.append(video_list)
  94. elif name and name in demand_name:
  95. rejected_substring_candidates += 1
  96. if not matched_pool_ids:
  97. video_updates[belong_id] = None
  98. continue
  99. matched_edges += len(matched_pool_ids)
  100. for pool_id in matched_pool_ids:
  101. candidate_pairs.append((belong_id, pool_id))
  102. video_updates[belong_id] = _merge_video_ids(matched_video_lists)
  103. with get_session() as session:
  104. rel_repo = DemandBelongPoolRelRepository(session)
  105. replaced_edges = rel_repo.delete_by_pool_ids(
  106. pool_id for pool_id, _, _ in pool_rows
  107. )
  108. insert_rows = [
  109. {
  110. "demand_belong_category_id": belong_id,
  111. "multi_demand_pool_di_id": pool_id,
  112. "biz_dt": biz_dt,
  113. "relation_type": "explicit_token",
  114. "relation_source": "demand_pool_name_tokens",
  115. "reason": "需求归属词完整命中上游 demand_name 的显式空格分词",
  116. "confidence": 1.0,
  117. "is_inferred": True,
  118. "status": "active",
  119. "valid_from_biz_dt": biz_dt,
  120. "valid_to_biz_dt": None,
  121. }
  122. for belong_id, pool_id in candidate_pairs
  123. ]
  124. inserted = rel_repo.bulk_insert_ignore(insert_rows)
  125. video_updated = DemandBelongCategoryRepository(session).update_video_lists(
  126. video_updates
  127. )
  128. result = {
  129. "biz_dt": biz_dt,
  130. "words": len(words),
  131. "pool_rows": len(pool_rows),
  132. "matched_edges": matched_edges,
  133. "candidate_pairs": len(candidate_pairs),
  134. "rejected_substring_candidates": rejected_substring_candidates,
  135. "replaced_edges": replaced_edges,
  136. "inserted": inserted,
  137. "video_updated": video_updated,
  138. }
  139. logger.info("demand_belong_pool_rel sync completed: %s", result)
  140. return result