|
|
@@ -1,8 +1,8 @@
|
|
|
"""Phase 1: 从 demand_search_queue 取搜索词 → 微信搜索 → 写入 demand_search_article_relation
|
|
|
|
|
|
流程:
|
|
|
- ArticleSearchRelationMapper.fetch_queue_pending() → 微信搜索 → RelationMapper.insert_batch()
|
|
|
- experiment_id 从上到下透传,不做过滤条件。
|
|
|
+ ArticleSearchRelationMapper.fetch_queue_pending(dt) → 微信搜索 → RelationMapper.insert_batch()
|
|
|
+ 每个队列项包含单条 search_key + key_type(由 video_vectors 解构驱动),experiment_id 透传。
|
|
|
"""
|
|
|
|
|
|
import asyncio
|
|
|
@@ -18,18 +18,11 @@ from ._utils import parse_search_result, is_valid_keyword
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
-# demand_search_queue 列名 → key_type 映射
|
|
|
-_SEARCH_KEY_COLUMNS = [
|
|
|
- ("standard_element", DemandSearchArticleConst.KeyType.STANDARD_ELEMENT),
|
|
|
- ("match_text", DemandSearchArticleConst.KeyType.MATCH_TEXT),
|
|
|
- ("match_generalized_element", DemandSearchArticleConst.KeyType.MATCH_GENERALIZED_ELEMENT),
|
|
|
-]
|
|
|
-
|
|
|
|
|
|
class DemandSearchArticle(DemandSearchArticleConst):
|
|
|
"""从需求队列取搜索词 → 微信搜索 → 写入关系表
|
|
|
|
|
|
- experiment_id 只透传不过滤——取 match_score 最高的待处理项。
|
|
|
+ 按 dt 分区读取,experiment_id 只透传不过滤。
|
|
|
"""
|
|
|
|
|
|
def __init__(self, pool: MysqlManager):
|
|
|
@@ -39,13 +32,15 @@ class DemandSearchArticle(DemandSearchArticleConst):
|
|
|
# 入口
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
- async def deal(self, *, limit: int = 500) -> dict:
|
|
|
- """按 match_score 降序取待处理项 → 微信搜索 → 写入 relation
|
|
|
+ async def deal(self, dt: str | None = None, *, limit: int = 500) -> dict:
|
|
|
+ """取待处理项(id ASC FIFO)→ 微信搜索 → 写入 relation
|
|
|
+
|
|
|
+ 传 dt 则按分区过滤,不传则全表扫描。
|
|
|
|
|
|
Returns:
|
|
|
{"queued": int, "processed": int, "results_written": int}
|
|
|
"""
|
|
|
- items = await self.relation_mapper.fetch_queue_pending(limit=limit)
|
|
|
+ items = await self.relation_mapper.fetch_queue_pending(dt, limit=limit)
|
|
|
if not items:
|
|
|
logger.info("无待处理队列项")
|
|
|
return {"queued": 0, "processed": 0, "results_written": 0}
|
|
|
@@ -56,9 +51,8 @@ class DemandSearchArticle(DemandSearchArticleConst):
|
|
|
total_written = 0
|
|
|
processed = 0
|
|
|
for item in items:
|
|
|
- n = 0
|
|
|
try:
|
|
|
- n = await self._search_and_write(item)
|
|
|
+ n, from_cache_only = await self._search_and_write(item)
|
|
|
except Exception as e:
|
|
|
logger.exception(
|
|
|
"搜索失败: queue_id=%s, experiment_id=%s",
|
|
|
@@ -72,10 +66,13 @@ class DemandSearchArticle(DemandSearchArticleConst):
|
|
|
if n > 0:
|
|
|
total_written += n
|
|
|
processed += 1
|
|
|
- await self.relation_mapper.queue_mark_done(item["id"])
|
|
|
+ await self.relation_mapper.queue_mark_done(item["id"], remark="搜索成功")
|
|
|
+ elif from_cache_only:
|
|
|
+ processed += 1
|
|
|
+ await self.relation_mapper.queue_mark_done(item["id"], remark="命中缓存")
|
|
|
else:
|
|
|
await self.relation_mapper.queue_mark_failed(
|
|
|
- item["id"], reason="所有搜索词均未返回结果",
|
|
|
+ item["id"], reason="搜索词未返回结果",
|
|
|
)
|
|
|
|
|
|
logger.info(
|
|
|
@@ -88,39 +85,48 @@ class DemandSearchArticle(DemandSearchArticleConst):
|
|
|
"results_written": total_written,
|
|
|
}
|
|
|
|
|
|
- async def _search_and_write(self, item: Dict) -> int:
|
|
|
- """对单个队列项的每个搜索词列执行搜索,累计写入行数"""
|
|
|
- total = 0
|
|
|
- for col, key_type in _SEARCH_KEY_COLUMNS:
|
|
|
- keyword = (item.get(col) or "").strip()
|
|
|
- if not is_valid_keyword(keyword):
|
|
|
- continue
|
|
|
- n = await self._search_one_keyword(
|
|
|
- keyword=keyword,
|
|
|
- key_type=key_type,
|
|
|
- experiment_id=item["experiment_id"],
|
|
|
- )
|
|
|
- total += n
|
|
|
- return total
|
|
|
+ async def _search_and_write(self, item: Dict) -> tuple[int, bool]:
|
|
|
+ """对单个队列项的 search_key 执行搜索
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ (newly_written, from_cache_only) — from_cache_only 表示有数据但全部来自缓存命中。
|
|
|
+ """
|
|
|
+ keyword = (item.get("search_key") or "").strip()
|
|
|
+ if not is_valid_keyword(keyword):
|
|
|
+ return 0, False
|
|
|
+ return await self._search_one_keyword(
|
|
|
+ keyword=keyword,
|
|
|
+ key_type=item["key_type"],
|
|
|
+ experiment_id=item["experiment_id"],
|
|
|
+ )
|
|
|
|
|
|
async def _search_one_keyword(
|
|
|
self,
|
|
|
keyword: str,
|
|
|
key_type: str,
|
|
|
experiment_id: str,
|
|
|
- ) -> int:
|
|
|
- """单个搜索词: 翻页搜索 → 批量写入 relation 表"""
|
|
|
- total = 0
|
|
|
+ ) -> tuple[int, bool]:
|
|
|
+ """单个搜索词: 翻页搜索 → 批量写入 relation 表
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ (newly_written, from_cache_only) — from_cache_only 为 True 表示
|
|
|
+ 至少有一页返回了文章,且没有新写入(全部来自缓存命中)。
|
|
|
+ """
|
|
|
+ newly_written = 0
|
|
|
+ had_data = False
|
|
|
cursor = "0"
|
|
|
for page in range(self.MAX_SEARCH_PAGES):
|
|
|
result, from_cache = await self._search_with_cache(keyword, page, cursor)
|
|
|
if result is None:
|
|
|
break
|
|
|
|
|
|
+ data = result.get("data") or {}
|
|
|
+ articles = data.get("data") or []
|
|
|
+ if articles:
|
|
|
+ had_data = True
|
|
|
+
|
|
|
# 非缓存命中时才写 relation + 等待(缓存命中的数据已经写过了)
|
|
|
if not from_cache:
|
|
|
- data = result.get("data", {})
|
|
|
- articles = data.get("data", [])
|
|
|
if not articles:
|
|
|
break
|
|
|
|
|
|
@@ -129,22 +135,21 @@ class DemandSearchArticle(DemandSearchArticleConst):
|
|
|
for a in articles
|
|
|
]
|
|
|
n = await self.relation_mapper.insert_batch(items)
|
|
|
- total += n
|
|
|
+ newly_written += n
|
|
|
logger.info(
|
|
|
"搜索写入: keyword=%s, page=%d, 返回 %d 条, 写入 %d 条",
|
|
|
keyword, page, len(articles), n,
|
|
|
)
|
|
|
await asyncio.sleep(self.SEARCH_INTERVAL_SEC)
|
|
|
|
|
|
- # 从响应中取翻页游标(缓存和非缓存都要翻页)
|
|
|
- data = result.get("data", {})
|
|
|
has_more = data.get("has_more", False)
|
|
|
next_cursor = data.get("next_cursor")
|
|
|
if not has_more or not next_cursor:
|
|
|
break
|
|
|
cursor = str(next_cursor)
|
|
|
|
|
|
- return total
|
|
|
+ from_cache_only = had_data and newly_written == 0
|
|
|
+ return newly_written, from_cache_only
|
|
|
|
|
|
async def _search_with_cache(
|
|
|
self, keyword: str, page: int, cursor: str,
|