fix: 去重合并多来源后同步到 events/Qdrant/MySQL,新增 sync-sources 回填命令

- dedup/pipeline: 合并 source_ids 后自动同步已生成的 events JSON 和 Qdrant payload
- 新增 sync_sources_from_deduped / uv run en-news sync-sources,用于历史数据回填
- sync-sources 会扫描 deduped 多来源唯一篇,更新 events、Qdrant、MySQL news_event.sources
- docs: 补充 sync-sources 使用说明
This commit is contained in:
2026-08-23 10:55:23 +08:00
parent cf74e5e7f5
commit 9e4f5b4c75
4 changed files with 178 additions and 0 deletions
+20
View File
@@ -312,6 +312,26 @@ def report():
raise typer.Exit(code=1)
@app.command()
def sync_sources():
"""M3/M9 辅助:把 deduped 已合并的多来源同步到 events/Qdrant/MySQL"""
from dedup.pipeline import sync_sources_from_deduped
try:
stats = sync_sources_from_deduped()
typer.echo(
f"\n✅ 来源同步完成: 扫描 {stats['deduped_scanned']} 个唯一篇, "
f"多来源 {stats['multi_source']} 个, "
f"events 更新 {stats['events_updated']}, "
f"Qdrant 更新 {stats['qdrant_updated']}, "
f"MySQL 更新 {stats['db_updated']}"
)
except Exception as e:
logger.exception("来源同步失败")
typer.echo(f"❌ 来源同步出错: {e}", err=True)
raise typer.Exit(code=1)
@app.command()
def pipeline(
skip_report: bool = typer.Option(
+150
View File
@@ -172,10 +172,160 @@ def _merge_duplicate_source(article: ProcessedArticle, result: DedupResult) -> N
)
logger.debug("来源合并: %s → %s (sources=%s)",
article.source_id, result.matched_url_hash, merged)
# 同步到已生成的 events / Qdrant,避免“deduped 有多来源但 DB 没有”
_sync_merged_sources_downstream(result.matched_url_hash, merged)
except Exception as e:
logger.exception("来源合并失败 %s: %s", target, e)
def _sync_event_source_ids(url_hash: str, source_ids: list[str]) -> int:
"""把合并后的 source_ids 同步到已生成的 events JSON。"""
updated = 0
for ev_path in sorted(Path("data/events").glob(f"*/{url_hash}.json")):
try:
data = json.loads(ev_path.read_text(encoding="utf-8"))
if data.get("source_ids") != source_ids:
data["source_ids"] = list(source_ids)
ev_path.write_text(
json.dumps(data, indent=2, ensure_ascii=False),
encoding="utf-8",
)
logger.info("来源合并同步 events: %s -> %s", ev_path, source_ids)
updated += 1
except Exception as e:
logger.exception("同步 events 来源失败 %s: %s", ev_path, e)
return updated
def _sync_qdrant_source_ids(url_hash: str, source_ids: list[str]) -> int:
"""把合并后的 source_ids 同步到 Qdrant 中已存在的向量点。"""
emb_paths = sorted(Path("data/embeddings").glob(f"*/{url_hash}.json"))
if not emb_paths:
return 0
event_path = next(Path("data/events").glob(f"*/{url_hash}.json"), None)
if event_path is None:
return 0
try:
article_data = json.loads(event_path.read_text(encoding="utf-8"))
except Exception as e:
logger.warning("读取 events 失败,跳过 Qdrant 来源同步 %s: %s", event_path, e)
return 0
try:
from vectorstore.client import VectorStore, make_qdrant_client
from vectorstore.pipeline import _build_payload
client = make_qdrant_client()
store = VectorStore(client)
try:
payload = _build_payload(article_data)
for emb_path in emb_paths:
emb_data = json.loads(emb_path.read_text(encoding="utf-8"))
store.upsert([{
"id": url_hash,
"vector": emb_data["vector"],
"payload": payload,
}])
finally:
store.close()
logger.info("来源合并同步 Qdrant: %s -> %s", url_hash, source_ids)
return len(emb_paths)
except Exception as e:
logger.warning("同步 Qdrant 来源失败 %s: %s", url_hash, e)
return 0
def _sync_merged_sources_downstream(url_hash: str, source_ids: list[str]) -> None:
"""去重合并来源后,同步更新 events 与 Qdrant 中已存在的下游数据。"""
_sync_event_source_ids(url_hash, source_ids)
_sync_qdrant_source_ids(url_hash, source_ids)
def sync_sources_from_deduped() -> dict:
"""历史数据回填:扫描所有 deduped 唯一篇,把多来源同步到 events/Qdrant/MySQL。
该函数用于修复“deduped 已合并多来源,但旧 events/数据库未更新”的历史数据。
"""
stats = {
"deduped_scanned": 0,
"multi_source": 0,
"events_updated": 0,
"qdrant_updated": 0,
"db_updated": 0,
}
targets = sorted(Path("data/deduped").glob("*/uniques/*.json"))
for target in targets:
try:
data = json.loads(target.read_text(encoding="utf-8"))
except Exception:
continue
stats["deduped_scanned"] += 1
raw_ids = data.get("source_ids") or [data.get("source_id")]
source_ids = list(dict.fromkeys(x for x in raw_ids if x))
if len(source_ids) <= 1:
continue
stats["multi_source"] += 1
url_hash = data.get("url_hash", target.stem)
stats["events_updated"] += _sync_event_source_ids(url_hash, source_ids)
stats["qdrant_updated"] += _sync_qdrant_source_ids(url_hash, source_ids)
stats["db_updated"] += _update_db_sources(url_hash, source_ids)
logger.info(
"来源回填完成: scanned=%d multi=%d events=%d qdrant=%d db=%d",
stats["deduped_scanned"], stats["multi_source"],
stats["events_updated"], stats["qdrant_updated"], stats["db_updated"],
)
return stats
def _update_db_sources(url_hash: str, source_ids: list[str]) -> int:
"""根据最新 events 来源,更新 MySQL news_event 中该文章的 source/sources。"""
event_path = next(Path("data/events").glob(f"*/{url_hash}.json"), None)
if event_path is None:
return 0
try:
article_data = json.loads(event_path.read_text(encoding="utf-8"))
except Exception:
return 0
try:
from scheduler.reporter import _article_source_label, _article_sources_full
source_label = _article_source_label(article_data)
source_list = _article_sources_full(article_data)
if not source_label and not source_list:
return 0
from report_db import connect
conn = connect()
try:
with conn.cursor() as cur:
cur.execute(
"""
UPDATE news_event
SET source = %s, sources = %s
WHERE section = 'intl' AND url = %s
""",
(
source_label,
json.dumps(source_list, ensure_ascii=False) if source_list else None,
article_data.get("url"),
),
)
updated = cur.rowcount
conn.commit()
return updated or 0
finally:
conn.close()
except Exception as e:
logger.warning("同步 MySQL 来源失败 %s: %s", url_hash, e)
return 0
def _layer_num(result: DedupResult) -> int:
"""DedupResult → 命中层编号。"""
if result.matched_layer is None:
+4
View File
@@ -45,6 +45,10 @@ uv run en-news extract --source cnbc
2. 正文 hash 一致 → 重复
3. SimHash 汉明距离 ≤ 阈值 → 模糊重复
- 跨源合并:重复篇的来源 ID 会追加到保留唯一篇的 `source_ids`,日报可展示多来源。
- 合并后会同步更新已生成的 `events` 与 Qdrant payload;历史数据可执行:
```bash
uv run en-news sync-sources
```
```bash
uv run en-news dedup
+4
View File
@@ -12,6 +12,7 @@
| `index` | M6 写入 Qdrant | `--recreate` 重建;`--date/-d` 指定日期;`--all` 全量回灌 |
| `search` | M6 语义检索 | 必需 query;`--top-k` 返回条数 |
| `report` | M9 生成日报并入库 | 无 |
| `sync-sources` | 把 deduped 已合并的多来源同步到 events/Qdrant/MySQL | 无 |
| `pipeline` | M2→M6(+日报) | `--skip-report` 跳过日报;`--date/-d` 指定日期 |
| `mcp-server` | M8 启动 MCP 服务 | 无 |
@@ -38,6 +39,9 @@ uv run en-news index --date 20260801
# 全量回灌历史向量(--recreate 搭配 --all 时只会在首个日期重建 collection)
uv run en-news index --all
uv run en-news embed --all
# 修复历史多来源:把 deduped 中已合并的 source_ids 同步到 events/Qdrant/MySQL
uv run en-news sync-sources
```
## 2. Shell 脚本