update code level

This commit is contained in:
lychang
2026-07-21 03:50:02 +08:00
parent d64b85bc9a
commit 4321cfa62f
8 changed files with 39 additions and 574 deletions

View File

@@ -460,7 +460,7 @@ Base URL: `http://<host>:<port>/api`
## 管线
### POST /api/pipeline/run
手动触发完整管线。
手动触发完整管线(dict 优先匹配解析)。
**参数**:无
@@ -478,9 +478,6 @@ Base URL: `http://<host>:<port>/api`
"new_albums_found": 0,
"persons_updated": 0,
"tags_updated": 0,
"persons_merged": 0,
"aliases_moved": 0,
"tags_removed_person": 0,
"empty_albums_deleted": 0,
"dup_albums_deleted": 0,
"search_db_synced": 0,
@@ -522,8 +519,6 @@ Base URL: `http://<host>:<port>/api`
"success": true,
"albums_parsed": 100,
"persons_updated": 80,
"persons_merged": 5,
"aliases_moved": 3,
"non_persons_deleted": 2,
"stop_word_persons_removed": 1,
"synced_to_search_db": 100,

View File

@@ -1,7 +1,6 @@
//! 核心引擎层 — 标题解析、人名匹配、标签提取、推荐计算
//! 核心引擎层 — 标题解析、标签提取、推荐计算
pub mod title_parser;
pub mod person_merger;
pub mod tag_dict;
pub mod tag_extractor;
pub mod recommend;
@@ -9,7 +8,6 @@ pub mod pipeline;
pub mod crawler;
#[allow(unused)] pub use title_parser::*;
#[allow(unused)] pub use person_merger::*;
#[allow(unused)] pub use tag_dict::*;
#[allow(unused)] pub use tag_extractor::*;
pub use recommend::*;

View File

@@ -1,493 +0,0 @@
//! 人名匹配与合并
//! 对应 Python 的 match_person_from_title.py + merge_persons_by_core.py
//! 所有函数均为 async,因为底层 Database 操作是异步的
use crate::db::Database;
use crate::engine::title_parser::{extract_cn, is_excluded_name};
use std::collections::{HashMap, HashSet};
/// 匹配模式
#[derive(Debug, Clone)]
pub struct Pattern {
pub text: String,
pub person_name: String,
pub source: String, // "name" | "alias"
pub length: usize,
}
/// 匹配结果
#[derive(Debug, Clone)]
pub struct MatchResult {
pub matched: bool,
pub method: String,
pub person: Option<String>,
pub matched_text: Option<String>,
pub all_persons: Vec<String>,
pub multi_person: bool,
}
/// 深度匹配结果
#[derive(Debug, Clone)]
pub struct DeepMatchResult {
pub matched: bool,
pub method: String, // "jieba_extract" | "exact_name_match" | "alias_fuzzy_match"
pub person: Option<String>,
pub matched_text: Option<String>,
pub all_persons: Vec<String>,
pub multi_person: bool,
}
// ── Pass1: 收集模式 ──
/// 收集所有人名和别名,构建匹配模式列表
/// 按长度降序排列,确保长名优先匹配
pub async fn collect_person_patterns(db: &Database) -> Vec<Pattern> {
let mut patterns = Vec::new();
// 1. 收集所有人名
let rows = db.get_all_person_names().await;
for (name, _albums) in &rows {
if is_excluded_name(name) {
continue;
}
patterns.push(Pattern {
text: name.clone(),
person_name: name.clone(),
source: "name".to_string(),
length: name.len(),
});
}
// 2. 收集所有别名
let aliases = db.get_all_aliases().await;
for (alias_name, person_name) in &aliases {
if is_excluded_name(person_name) || is_excluded_name(alias_name) {
continue;
}
if alias_name != person_name {
patterns.push(Pattern {
text: alias_name.clone(),
person_name: person_name.clone(),
source: "alias".to_string(),
length: alias_name.len(),
});
}
}
// 3. 去重
let mut best_map: HashMap<String, Pattern> = HashMap::new();
for p in patterns {
let key = p.text.to_lowercase();
best_map.entry(key).or_insert(p);
}
let mut patterns: Vec<Pattern> = best_map.into_values().collect();
// 4. 按长度降序排列
patterns.sort_by(|a, b| b.length.cmp(&a.length));
patterns
}
/// Pass1: 简单子串匹配
pub fn match_title(title: &str, patterns: &[Pattern]) -> Option<(String, String, String)> {
if title.is_empty() {
return None;
}
for p in patterns {
if p.text.len() < 2 {
continue;
}
if title.contains(&p.text) {
return Some((p.text.clone(), p.person_name.clone(), p.source.clone()));
}
}
None
}
/// Pass2: 深度匹配(多重策略)
pub fn deep_match_title(
title: &str,
known_names: &HashSet<String>,
known_aliases: &HashSet<String>,
) -> DeepMatchResult {
let mut result = DeepMatchResult {
matched: false,
method: String::new(),
person: None,
matched_text: None,
all_persons: Vec::new(),
multi_person: false,
};
if title.is_empty() {
return result;
}
let title_clean = title.trim();
// 策略1: 用正则提取人名
let extracted = crate::engine::title_parser::extract_person_name_from_title(title_clean);
if let Some(e) = extracted.first() {
if known_names.contains(e) {
result.matched = true;
result.method = "jieba_extract".to_string();
result.person = Some(e.clone());
result.matched_text = Some(e.clone());
return result;
}
}
// 策略2: 标题中精确子串匹配已知人名(长名优先)
let mut found: Vec<String> = Vec::new();
let mut sorted_names: Vec<&String> = known_names.iter().collect();
sorted_names.sort_by(|a, b| b.len().cmp(&a.len()));
for name in &sorted_names {
if name.len() < 2 {
continue;
}
if title_clean.contains(name.as_str()) {
found.push(name.to_string());
}
}
if !found.is_empty() {
found.sort_by(|a, b| b.len().cmp(&a.len()));
let best = found[0].clone();
result.matched = true;
result.method = "exact_name_match".to_string();
result.person = Some(best.clone());
result.matched_text = Some(best);
result.all_persons = found.clone();
result.multi_person = found.len() > 1;
return result;
}
// 策略3: 别名模糊匹配
let title_lower = title_clean.to_lowercase();
let mut sorted_aliases: Vec<&String> = known_aliases.iter().collect();
sorted_aliases.sort_by(|a, b| b.len().cmp(&a.len()));
for alias in &sorted_aliases {
if alias.len() < 2 {
continue;
}
if title_lower.contains(alias.to_lowercase().as_str()) {
result.matched = true;
result.method = "alias_fuzzy_match".to_string();
result.person = Some(alias.to_string());
result.matched_text = Some(alias.to_string());
return result;
}
}
result
}
/// 收集所有已知人名和别名
pub async fn collect_known_names(db: &Database) -> (HashSet<String>, HashSet<String>) {
let mut known_names = HashSet::new();
let mut known_aliases = HashSet::new();
for (name, _) in db.get_all_person_names().await {
known_names.insert(name);
}
for (alias_name, _) in db.get_all_aliases().await {
known_aliases.insert(alias_name);
}
(known_names, known_aliases)
}
/// 检测含多个人名的相册
pub async fn detect_multi_person_titles(
db: &Database,
known_names: Option<&HashSet<String>>,
) -> Vec<(String, String, Vec<String>)> {
let names = match known_names {
Some(n) => n.clone(),
None => collect_known_names(db).await.0,
};
let albums = db.get_all_albums_full().await;
let mut multi_person = Vec::new();
for album in &albums {
let title = album.title.as_deref().unwrap_or("");
if title.is_empty() {
continue;
}
let mut found: Vec<String> = Vec::new();
let mut sorted_names: Vec<&String> = names.iter().collect();
sorted_names.sort_by(|a, b| b.len().cmp(&a.len()));
for name in &sorted_names {
if name.len() < 2 {
continue;
}
if title.contains(name.as_str()) {
found.push(name.to_string());
}
}
if found.len() > 1 {
multi_person.push((
album.id.clone(),
title.to_string(),
found,
));
}
}
multi_person
}
// ── 中文核心合并分析 ──
/// 分析所有人名的中文核心分组
/// 返回: {core: [(id, name, albums, images), ...]}
pub async fn analyze_groups(db: &Database) -> HashMap<String, Vec<(i64, String, i64, i64)>> {
let persons = db.get_all_persons_with_counts().await;
let mut groups: HashMap<String, Vec<(i64, String, i64, i64)>> = HashMap::new();
for (pid, name, albums, images) in persons {
if let Some(cn) = extract_cn(&name) {
if cn.len() >= 2 {
groups.entry(cn).or_default().push((pid, name, albums, images));
}
}
}
for items in groups.values_mut() {
items.sort_by(|a, b| b.2.cmp(&a.2));
}
groups.retain(|_, v| v.len() >= 2);
groups
}
/// 合并结果统计
#[derive(Debug, Clone, Default)]
pub struct MergeResult {
pub groups_analyzed: usize,
pub persons_merged: usize,
pub aliases_moved: usize,
}
/// 执行合并:扫描所有可合并的人物组,合并到相册数最多的那个
///
/// 合并分两个阶段:
/// Phase 1: 中文核心相同 → 合并到相册数最多的
/// Phase 2: 名字包含关系(A包含B,且B更短)→ A合并到B(短名保留)
///
/// 返回合并统计
pub async fn execute_merge(db: &Database) -> MergeResult {
let mut result = MergeResult::default();
// ── Phase 1: 中文核心合并 ──
let groups = analyze_groups(db).await;
result.groups_analyzed = groups.len();
for (_core, items) in &groups {
if items.len() < 2 {
continue;
}
// 第一个(相册数最多)作为目标
let dst_id = items[0].0;
let dst_name = &items[0].1;
// 其余的合并到目标
for (src_id, src_name, _, _) in &items[1..] {
println!("[合并·核心] {} (id={}) → {} (id={})", src_name, src_id, dst_name, dst_id);
let src_aliases = db.get_person_aliases(*src_id).await;
let alias_count_before = src_aliases.len();
db.merge_person_into(*src_id, dst_id).await;
result.persons_merged += 1;
let moved = alias_count_before + 1;
result.aliases_moved += moved;
}
}
// ── Phase 2: 包含关系合并 ──
// 重新获取当前所有人物(Phase 1 后可能有变化)
let mut persons = db.get_all_persons_with_counts().await;
// 过滤掉排除名单中的
persons.retain(|(_, name, _, _)| !is_excluded_name(name));
// 按名字长度升序排列(短名在前,作为合并目标候选)
persons.sort_by(|a, b| a.1.len().cmp(&b.1.len()));
// 用一个 set 记录已被合并的 id,避免重复操作
let mut merged_ids: HashSet<i64> = HashSet::new();
for i in 0..persons.len() {
let (dst_id, dst_name, _dst_albums, _) = &persons[i];
if merged_ids.contains(dst_id) { continue; }
if dst_name.len() < 2 { continue; }
// 找所有比 dst_name 长且包含 dst_name 的人
let dst_lower = dst_name.to_lowercase();
for j in (i + 1)..persons.len() {
let (src_id, src_name, _, _) = &persons[j];
if merged_ids.contains(src_id) { continue; }
if src_name.len() <= dst_name.len() { continue; }
let src_lower = src_name.to_lowercase();
if src_lower.contains(&dst_lower) {
// 额外保护:dst 的中文核心必须和 src 的中文核心一致,
// 或者 dst 完全匹配 src 的开头/结尾,避免误合并(如 "小美" 包含 "小")
let dst_cn = extract_cn(dst_name).unwrap_or_default();
let src_cn = extract_cn(src_name).unwrap_or_default();
let cn_ok = !dst_cn.is_empty() && !src_cn.is_empty() && src_cn.contains(&dst_cn);
let starts_with = src_lower.starts_with(&dst_lower);
let ends_with = src_lower.ends_with(&dst_lower);
if !cn_ok && !starts_with && !ends_with {
continue;
}
println!("[合并·包含] {} (id={}) → {} (id={})", src_name, src_id, dst_name, dst_id);
let src_aliases = db.get_person_aliases(*src_id).await;
let alias_count_before = src_aliases.len();
db.merge_person_into(*src_id, *dst_id).await;
merged_ids.insert(*src_id);
result.persons_merged += 1;
let moved = alias_count_before + 1;
result.aliases_moved += moved;
}
}
}
if result.persons_merged > 0 {
println!("[合并] 完成:分析 {} 组,合并 {} 个人物,迁移 {} 个别名",
result.groups_analyzed, result.persons_merged, result.aliases_moved);
}
result
}
/// 统计匹配分析
#[derive(Debug, Clone)]
pub struct MatchStats {
pub total_albums: usize,
pub matched: usize,
pub unmatched: usize,
pub already_set: usize,
pub changed: usize,
pub by_source: HashMap<String, usize>,
pub name_frequency: HashMap<String, usize>,
}
/// 执行 Pass1 匹配分析
pub async fn analyze_matches(db: &Database, patterns: &[Pattern]) -> MatchStats {
let albums = db.get_all_albums_full().await;
let mut stats = MatchStats {
total_albums: albums.len(),
matched: 0,
unmatched: 0,
already_set: 0,
changed: 0,
by_source: HashMap::new(),
name_frequency: HashMap::new(),
};
for album in &albums {
let title = album.title.as_deref().unwrap_or("");
let current = ""; // person_name_original 已迁移到 search.db
if let Some((_text, person, source)) = match_title(title, patterns) {
*stats.by_source.entry(source).or_insert(0) += 1;
*stats.name_frequency.entry(person.clone()).or_insert(0) += 1;
if current.is_empty() {
stats.matched += 1;
} else if current != person {
stats.changed += 1;
} else {
stats.already_set += 1;
}
} else {
stats.unmatched += 1;
}
}
stats
}
/// 深度匹配分析统计
#[derive(Debug, Clone)]
pub struct DeepMatchStats {
pub total_albums: usize,
pub pass1_matched: usize,
pub pass1_unmatched: usize,
pub pass2_jieba_match: usize,
pub pass2_exact_match: usize,
pub pass2_alias_match: usize,
pub pass2_still_unmatched: usize,
pub pass2_multi_person: usize,
}
/// 执行 Pass2 深度匹配分析
pub async fn analyze_deep_matches(db: &Database) -> DeepMatchStats {
let (known_names, known_aliases) = collect_known_names(db).await;
let albums = db.get_all_albums_full().await;
let mut stats = DeepMatchStats {
total_albums: albums.len(),
pass1_matched: 0,
pass1_unmatched: 0,
pass2_jieba_match: 0,
pass2_exact_match: 0,
pass2_alias_match: 0,
pass2_still_unmatched: 0,
pass2_multi_person: 0,
};
for album in &albums {
let title = album.title.as_deref().unwrap_or("");
let current = ""; // person_name_original 已迁移到 search.db
if title.is_empty() {
continue;
}
if !current.is_empty() && known_names.contains(current) {
stats.pass1_matched += 1;
continue;
}
stats.pass1_unmatched += 1;
let result = deep_match_title(title, &known_names, &known_aliases);
if !result.matched {
stats.pass2_still_unmatched += 1;
continue;
}
match result.method.as_str() {
"jieba_extract" => stats.pass2_jieba_match += 1,
"exact_name_match" => stats.pass2_exact_match += 1,
"alias_fuzzy_match" => stats.pass2_alias_match += 1,
_ => {}
}
if result.multi_person {
stats.pass2_multi_person += 1;
}
}
stats
}

View File

@@ -1,20 +1,20 @@
//! 处理管线 — 批量处理相册元数据
//!
//! 核心架构:脏集追踪 + 增量解析
//! 核心架构:脏集追踪 + 增量解析,dict 优先匹配
//!
//! 精简后流程(6 阶段,2 次全库扫描):
//! Phase ① 合并预处理(清洗标点/短标签/人名标签/新相册)→ 1次相册扫描
//! Phase ② 合并人物清理(非人物/停用词人物) → 1次人物扫描
//! Phase ③ 增量解析脏集相册 → 只扫脏集
//! Phase ④ 人物合并 + 重计 → 1次人物扫描
//! Phase ⑤ 后校验标签中的人名 → 可优化
//! 流程:
//! Phase Z0 删除空相册
//! Phase Z1 去重同名相册
//! Phase ① 收集新相册(未处理)
//! Phase ② 合并人物清理(非人物/停用词人物)→ 1次人物扫描
//! Phase ③ dict 优先解析脏集相册 → 只扫脏集
//! Phase C 全量重计人物计数
//!
//! 脏集(DirtySet)贯穿各阶段,每个阶段发现的"需要重新解析"的相册都会加入脏集,
//! 最终只处理脏集里的相册,而非全库扫描,大幅提升效率。
use crate::db::Database;
use crate::engine::title_parser;
use crate::engine::person_merger;
use crate::engine::tag_dict;
use crate::search_db::SearchDb;
use std::collections::HashSet;
@@ -76,16 +76,19 @@ pub async fn phase_collect_new_albums(db: &Database) -> DirtySet {
// ┌─ Phase 1a:并行解析(CPU 密集,rayon par_iter)
// └─ Phase 1b:批量写库(IO 密集,一个事务)
pub async fn phase_parse_dirty(db: &Database, dirty_set: &DirtySet) -> (usize, usize) {
pub async fn phase_parse_dirty(db: &Database, search_db: &SearchDb, dirty_set: &DirtySet) -> (usize, usize) {
if dirty_set.is_empty() {
println!("[管线·Phase1] 增量解析跳过:脏集为空");
println!("[管线·Phase3] dict 优先解析跳过:脏集为空");
return (0, 0);
}
let dirty_ids: Vec<String> = dirty_set.clone().into_inner().into_iter().collect();
let albums = db.get_albums_by_ids(&dirty_ids).await;
// ── Phase 1a:并行解析(CPU bound,rayon) ──
// 加载 search.db 的 dict 条目(提供 merged_to 别名解析)
let dict_entries = search_db.load_dict_all().await.unwrap_or_default();
// ── Phase 3a:并行解析(CPU bound,rayon) ──
let dirty_albums: Vec<&crate::models::AlbumMeta> = albums.iter()
.filter(|a| dirty_set.contains(&a.id))
.collect();
@@ -100,22 +103,23 @@ pub async fn phase_parse_dirty(db: &Database, dirty_set: &DirtySet) -> (usize, u
.filter_map(|album| {
let title = album.title.as_deref()?;
if title.is_empty() { return None; }
Some(title_parser::parse_album_title(&album.id, title))
// 使用 dict 优先匹配解析
Some(title_parser::parse_album_title_with_dict(&album.id, title, &dict_entries))
})
.collect();
let person_count = parsed.iter().filter(|r| !r.person_names.is_empty()).count();
let tag_count = parsed.iter().filter(|r| !r.tags.is_empty()).count();
// ── Phase 1b:批量写入 ──
// ── Phase 3b:批量写入 ──
let _guard = db.write_lock.lock().await;
let mut tx = db.pool.begin().await.expect("begin tx");
db.batch_write_parse_results(&mut tx, &parsed).await.unwrap_or_else(|e| {
eprintln!("[管线·Phase1b] 批量写入失败: {}", e);
eprintln!("[管线·Phase3b] 批量写入失败: {}", e);
});
tx.commit().await.expect("commit tx");
println!("[管线·Phase1] 并行解析+批量写入完成: 脏集 {} 个, 有效 {} 个 (跳过 {} 无标题), 更新人名 {} 个, 更新标签 {} 个",
println!("[管线·Phase3] dict 优先解析+批量写入完成: 脏集 {} 个, 有效 {} 个 (跳过 {} 无标题), 更新人名 {} 个, 更新标签 {} 个",
total, parsed.len(), skipped, person_count, tag_count);
(person_count, tag_count)
}
@@ -149,29 +153,7 @@ pub async fn phase_recount_all(db: &Database) -> usize {
fixed
}
// ════════════════════════════════════════════════
pub async fn phase_merge_persons(db: &Database) -> (usize, usize) {
let result = person_merger::execute_merge(db).await;
if result.persons_merged > 0 {
println!("[管线·Phase2] 人物合并完成:分析 {} 组,合并 {} 人,迁移 {} 个别名",
result.groups_analyzed, result.persons_merged, result.aliases_moved);
} else {
println!("[管线·Phase2] 人物合并完成:无需要合并的人物");
}
(result.persons_merged, result.aliases_moved)
}
// ════════════════════════════════════════════════
// Phase B:后校验标签 — 用合并后的人名检查
// ════════════════════════════════════════════════
pub async fn phase_clean_tags_after_parse(db: &Database) -> usize {
println!("[管线·PhaseB] 后校验标签完成:已迁移到 search.db,跳过");
0
}
// ════════════════════════════════════════════════
// 合并阶段 — 减少全库扫描次数
// Preprocess + Dirty Tracking
// ════════════════════════════════════════════════
/// 合并预处理结果
@@ -318,22 +300,18 @@ pub struct PipelineResult {
pub new_albums_found: usize, // Phase ①
pub persons_updated: usize, // Phase ③
pub tags_updated: usize, // Phase ③
pub persons_merged: usize, // Phase ④
pub aliases_moved: usize, // Phase ④
pub tags_removed_person: usize, // Phase ⑤
pub empty_albums_deleted: usize, // Phase Z0
pub dup_albums_deleted: usize, // Phase Z1
}
/// 一键执行精简管线
/// 一键执行精简管线(dict 优先匹配)
///
/// Phase Z0 删除空相册
/// Phase Z1 去重同名相册
/// Phase ① 合并预处理(清洗+短标签+人名标签+新相册) → 1次相册扫描
/// Phase ② 合并人物清理(非人物+停用词人物) → 1次人物扫描
/// Phase ③ 增量解析脏集 → 只扫脏集
/// Phase ④ 人物合并 + 重计 → 1次人物扫描
/// Phase ⑤ 后校验标签中的人名
/// Phase ① 收集新相册
/// Phase ② 合并人物清理(非人物+停用词人物) → 1次人物扫描
/// Phase ③ dict 优先解析脏集 → 只扫脏集
/// Phase C 全量重计
pub async fn run_pipeline(db: &Database, search_db: &SearchDb) -> PipelineResult {
// Phase Z0:删除空相册
let empty_albums_deleted = phase_delete_empty_albums(db).await;
@@ -341,7 +319,7 @@ pub async fn run_pipeline(db: &Database, search_db: &SearchDb) -> PipelineResult
// Phase Z1:去重同名相册(含删除文件夹和 search.db 数据)
let dup_albums_deleted = phase_dedup_albums(db, search_db).await;
// Phase ①:合并预处理(1 次相册扫描)
// Phase ①:收集新相册
let pre = phase_preprocess_combined(db).await;
let tags_cleaned = pre.tags_cleaned;
let short_tags_removed = pre.short_tags_removed;
@@ -357,23 +335,16 @@ pub async fn run_pipeline(db: &Database, search_db: &SearchDb) -> PipelineResult
master_dirty.extend(dirty_persons.into_inner());
let dirty_albums_detected = master_dirty.len();
// Phase ③:增量解析
let (persons_updated, tags_updated) = phase_parse_dirty(db, &master_dirty).await;
// Phase ③:dict 优先解析脏集
let (persons_updated, tags_updated) = phase_parse_dirty(db, search_db, &master_dirty).await;
// Phase ④:人物合并(内部会做包含关系 + 核心合并)
let (persons_merged, aliases_moved) = phase_merge_persons(db).await;
// Phase C:全量重计(合并后修正计数)
// Phase C:全量重计(修正计数)
let _recounted = phase_recount_all(db).await;
// Phase ⑤:后校验
let tags_removed_person = phase_clean_tags_after_parse(db).await;
PipelineResult {
tags_cleaned, short_tags_removed, person_like_tags_removed, dirty_albums_detected,
non_persons_deleted, stop_word_persons_removed, new_albums_found,
persons_updated, tags_updated, persons_merged,
aliases_moved, tags_removed_person,
persons_updated, tags_updated,
empty_albums_deleted, dup_albums_deleted,
}
}

View File

@@ -170,7 +170,7 @@ async fn main() -> std::io::Result<()> {
let new_albums = engine::pipeline::phase_collect_new_albums(&timer_db).await;
if !new_albums.is_empty() {
info!("[定时] 发现 {} 个新相册,增量解析...", new_albums.len());
engine::pipeline::phase_parse_dirty(&timer_db, &new_albums).await;
engine::pipeline::phase_parse_dirty(&timer_db, &timer_search_db, &new_albums).await;
info!("[定时] 同步到 search.db...");
engine::pipeline::sync_dirty_albums(&timer_db, &timer_search_db, &new_albums).await;
}

View File

@@ -154,7 +154,7 @@ pub async fn delete_person(
if !album_ids.is_empty() {
let mut dirty = engine::pipeline::DirtySet::new();
for aid in &album_ids { dirty.add(aid); }
let (persons_updated, tags_updated) = engine::pipeline::phase_parse_dirty(&db, &dirty).await;
let (persons_updated, tags_updated) = engine::pipeline::phase_parse_dirty(&db, &search_db, &dirty).await;
let synced = engine::pipeline::sync_dirty_albums(&db, &search_db, &dirty).await;
// 5. 重载内存词典

View File

@@ -22,19 +22,15 @@ pub async fn pipeline_run(
"new_albums_found": result.new_albums_found,
"persons_updated": result.persons_updated,
"tags_updated": result.tags_updated,
"persons_merged": result.persons_merged,
"aliases_moved": result.aliases_moved,
"tags_removed_person": result.tags_removed_person,
"empty_albums_deleted": result.empty_albums_deleted,
"dup_albums_deleted": result.dup_albums_deleted,
"search_db_synced": synced,
"message": format!(
"管线执行完毕:删除空相册{},去重{},清洗标点{}相册,移除短标签{},检测人名标签{},脏集{}相册,删除非人物{},清除停用词重叠人物{},发现新相册{},更新人名{},更新标签{},合并人物{},迁移别名{},后移除人名标签{},同步标签/人物到search.db{}",
"管线执行完毕:删除空相册{},去重{},清洗标点{}相册,移除短标签{},检测人名标签{},脏集{}相册,删除非人物{},清除停用词重叠人物{},发现新相册{},更新人名{},更新标签{},同步标签/人物到search.db{}",
result.empty_albums_deleted, result.dup_albums_deleted,
result.tags_cleaned, result.short_tags_removed, result.person_like_tags_removed, result.dirty_albums_detected,
result.non_persons_deleted, result.stop_word_persons_removed, result.new_albums_found,
result.persons_updated, result.tags_updated,
result.persons_merged, result.aliases_moved, result.tags_removed_person, synced
result.persons_updated, result.tags_updated, synced
)
}))
}
@@ -96,15 +92,13 @@ pub async fn pipeline_reparse_persons(
"success": true,
"albums_parsed": result.new_albums_found,
"persons_updated": result.persons_updated,
"persons_merged": result.persons_merged,
"aliases_moved": result.aliases_moved,
"non_persons_deleted": result.non_persons_deleted,
"stop_word_persons_removed": result.stop_word_persons_removed,
"synced_to_search_db": synced,
"message": format!(
"人物重新解析完成:重置{}个相册,解析{}个,更新人物{}个,合并{}个,迁移别名{}个,删除非人物{},清除重叠{},同步{}个到search.db",
"人物重新解析完成:重置{}个相册,解析{}个,更新人物{}个,删除非人物{},清除重叠{},同步{}个到search.db",
reset_count, result.new_albums_found,
result.persons_updated, result.persons_merged, result.aliases_moved,
result.persons_updated,
result.non_persons_deleted, result.stop_word_persons_removed, synced,
)
}))

View File

@@ -89,7 +89,7 @@ pub async fn delete_tag(
// 3. 重新解析受影响相册
let mut dirty = engine::pipeline::DirtySet::new();
for aid in &album_ids { dirty.add(aid); }
let (persons_updated, tags_updated) = engine::pipeline::phase_parse_dirty(&db, &dirty).await;
let (persons_updated, tags_updated) = engine::pipeline::phase_parse_dirty(&db, &search_db, &dirty).await;
// 4. 同步到 search.db
let synced = engine::pipeline::sync_dirty_albums(&db, &search_db, &dirty).await;