update code level

This commit is contained in:
lychang
2026-07-21 04:21:57 +08:00
parent d86cce34bc
commit f6c7baee5e
13 changed files with 357 additions and 313 deletions

7
Cargo.lock generated
View File

@@ -2387,6 +2387,12 @@ dependencies = [
"digest 0.11.3",
]
[[package]]
name = "sha1_smol"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
[[package]]
name = "sha2"
version = "0.10.9"
@@ -3133,6 +3139,7 @@ dependencies = [
"getrandom 0.4.3",
"js-sys",
"serde_core",
"sha1_smol",
"wasm-bindgen",
]

View File

@@ -11,7 +11,7 @@ serde_json = "1"
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio", "migrate"] }
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1.0", features = ["v4", "serde"] }
uuid = { version = "1.0", features = ["v4", "v5", "serde"] }
image = { version = "0.24", default-features = false, features = ["jpeg", "png", "gif", "webp"] }
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "json"] }
config = "0.13"

28
src/cache/mod.rs vendored
View File

@@ -116,9 +116,9 @@ impl TagCache {
pub struct PersonCache {
pub persons: Vec<PersonWithCounts>,
/// 搜索索引:关键词小写 → person_id 集合
pub search_index: HashMap<String, Vec<i64>>,
pub search_index: HashMap<String, Vec<String>>,
/// person_id → person 投影(用于按 id 快速访问)
pub id_map: HashMap<i64, PersonWithCounts>,
pub id_map: HashMap<String, PersonWithCounts>,
pub last_updated: chrono::DateTime<chrono::Utc>,
}
@@ -132,31 +132,31 @@ impl PersonCache {
let name_lower = p.name.to_lowercase();
for word in name_lower.split_whitespace() {
if word.len() >= 2 {
self.search_index.entry(word.to_string()).or_default().push(p.id);
self.search_index.entry(word.to_string()).or_default().push(p.id.clone());
}
}
self.search_index.entry(name_lower.clone()).or_default().push(p.id);
self.search_index.entry(name_lower.clone()).or_default().push(p.id.clone());
// 显示名
if let Some(ref dn) = p.display_name {
let dn_lower = dn.to_lowercase();
for word in dn_lower.split_whitespace() {
if word.len() >= 2 {
self.search_index.entry(word.to_string()).or_default().push(p.id);
self.search_index.entry(word.to_string()).or_default().push(p.id.clone());
}
}
self.search_index.entry(dn_lower).or_default().push(p.id);
self.search_index.entry(dn_lower).or_default().push(p.id.clone());
}
// 别名
for alias in &p.aliases {
let alias_lower = alias.to_lowercase();
for word in alias_lower.split_whitespace() {
if word.len() >= 2 {
self.search_index.entry(word.to_string()).or_default().push(p.id);
self.search_index.entry(word.to_string()).or_default().push(p.id.clone());
}
}
self.search_index.entry(alias_lower).or_default().push(p.id);
self.search_index.entry(alias_lower).or_default().push(p.id.clone());
}
self.id_map.insert(p.id, p.clone());
self.id_map.insert(p.id.clone(), p.clone());
}
self.persons = persons;
@@ -175,7 +175,7 @@ impl PersonCache {
// 1. 完整匹配
if let Some(ids) = self.search_index.get(&q) {
for id in ids {
if seen.insert(*id) {}
if seen.insert(id.clone()) {}
}
}
// 2. 单词匹配
@@ -183,7 +183,7 @@ impl PersonCache {
if word.len() >= 2 {
if let Some(ids) = self.search_index.get(word) {
for id in ids {
seen.insert(*id);
seen.insert(id.clone());
}
}
}
@@ -195,7 +195,7 @@ impl PersonCache {
p.display_name.as_deref().unwrap_or(""),
p.aliases.join(" ").to_lowercase());
if full.contains(&q) {
seen.insert(p.id);
seen.insert(p.id.clone());
}
}
}
@@ -268,7 +268,7 @@ pub async fn refresh_all_caches(
for p in &mut persons {
let actual: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM album_persons WHERE person_id = ?"
).bind(p.id).fetch_one(&search_db.pool).await.unwrap_or(0);
).bind(&p.id).fetch_one(&search_db.pool).await.unwrap_or(0);
if actual > 0 {
p.total_albums = actual;
}
@@ -285,7 +285,7 @@ pub async fn refresh_persons_cache(search_db: &SearchDb, persons_cache: &Arc<RwL
for p in &mut persons {
let actual: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM album_persons WHERE person_id = ?"
).bind(p.id).fetch_one(&search_db.pool).await.unwrap_or(0);
).bind(&p.id).fetch_one(&search_db.pool).await.unwrap_or(0);
if actual > 0 {
p.total_albums = actual;
}

View File

@@ -74,7 +74,7 @@ impl Database {
rows.iter().map(|r| {
let pid = r.get::<i64, _>("id");
PersonWithCounts {
id: pid, name: r.get("name"), display_name: r.get("display_name"),
id: pid.to_string(), name: r.get("name"), display_name: r.get("display_name"),
total_albums: r.get("total_albums"), total_images: r.get("total_images"),
aliases: aliases.get(&pid).cloned().unwrap_or_default(),
}
@@ -91,7 +91,7 @@ impl Database {
rows.iter().map(|r| {
let pid = r.get::<i64, _>("id");
PersonWithCounts {
id: pid, name: r.get("name"), display_name: r.get("display_name"),
id: pid.to_string(), name: r.get("name"), display_name: r.get("display_name"),
total_albums: r.get("total_albums"), total_images: r.get("total_images"),
aliases: aliases.get(&pid).cloned().unwrap_or_default(),
}
@@ -226,10 +226,16 @@ impl Database {
// ── 写入 ──
pub async fn insert_person(&self, name: &str, display_name: Option<&str>) -> i64 {
if let Some(p) = self.get_person_by_name(name).await { return p.id; }
if let Some(p) = self.get_person_by_name_internal(name).await { return p; }
sqlx::query("INSERT OR IGNORE INTO persons (name, display_name) VALUES (?, ?)")
.bind(name).bind(display_name).execute(&self.pool).await.ok();
self.get_person_by_name(name).await.map(|p| p.id).unwrap_or(0)
self.get_person_by_name_internal(name).await.unwrap_or(0)
}
/// 内部方法:按名称查找人员,直接返回 i64 id(避免 String 转换开销)
async fn get_person_by_name_internal(&self, name: &str) -> Option<i64> {
sqlx::query_scalar::<_, i64>("SELECT id FROM persons WHERE name = ?")
.bind(name).fetch_optional(&self.pool).await.ok().flatten()
}
/// 与 insert_person 相同,但使用已有的事务
@@ -349,8 +355,9 @@ impl Database {
pub async fn get_person_by_id(&self, id: i64) -> Option<Person> {
let r = sqlx::query("SELECT * FROM persons WHERE id = ?").bind(id).fetch_optional(&self.pool).await.ok()??;
let db_id: i64 = r.get("id");
Some(Person {
id: r.get("id"), name: r.get("name"), display_name: r.get("display_name"),
id: db_id.to_string(), name: r.get("name"), display_name: r.get("display_name"),
total_albums: r.get("total_albums"), total_images: r.get("total_images"),
original_names: r.get("original_names"), created_at: r.get("created_at"), updated_at: r.get("updated_at"),
})

View File

@@ -111,14 +111,20 @@ pub async fn phase_parse_dirty(db: &Database, search_db: &SearchDb, dirty_set: &
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 3b:批量写入 ──
// ── Phase 3b:批量写入 imager.db ──
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!("[管线·Phase3b] 批量写入失败: {}", e);
eprintln!("[管线·Phase3b] imager.db 批量写入失败: {}", e);
});
tx.commit().await.expect("commit tx");
// ── Phase 3c:直接写入 search.db(使用相同解析结果,无需重解析) ──
match search_db.batch_sync_parse_results(&parsed).await {
Ok(n) => println!("[管线·Phase3c] search.db 同步完成:{} 个相册", n),
Err(e) => eprintln!("[管线·Phase3c] search.db 同步失败: {}", e),
}
println!("[管线·Phase3] dict 优先解析+批量写入完成: 脏集 {} 个, 有效 {} 个 (跳过 {} 无标题), 更新人名 {} 个, 更新标签 {} 个",
total, parsed.len(), skipped, person_count, tag_count);
(person_count, tag_count)
@@ -355,88 +361,33 @@ pub async fn run_pipeline(db: &Database, search_db: &SearchDb) -> PipelineResult
// 不同步 albums 表数据(albums 由 imager.db 管理)
// ════════════════════════════════════════════════
/// 全量同步:将所有相册的 tags/persons 解析结果写入 search.db 的关联表
/// 全量同步:将所有相册的 tags/persons 解析结果写入 search.db 的关联表(单事务批量)
pub async fn sync_albums_to_search_db(db: &Database, search_db: &SearchDb) -> usize {
let albums = db.get_all_albums_full().await;
let mut synced = 0usize;
let mut tag_cache: std::collections::HashMap<String, i64> = std::collections::HashMap::new();
let mut person_cache: std::collections::HashMap<String, i64> = std::collections::HashMap::new();
for album in &albums {
// ── 1. 写入 tags → album_tags ──
if let Some(title) = album.title.as_deref() {
if !title.is_empty() {
let parse_result = title_parser::parse_album_title(&album.id, title);
if !parse_result.tags.is_empty() {
search_db.clear_album_tags(&album.id).await.ok();
for (tag_name, category) in &parse_result.tags {
let tag_id = match tag_cache.get(tag_name) {
Some(&id) => id,
None => {
match search_db.insert_tag(tag_name, Some(category)).await {
Ok(id) => { tag_cache.insert(tag_name.clone(), id); id }
Err(_) => continue,
}
}
};
search_db.insert_album_tag(&album.id, tag_id).await.ok();
}
}
}
// 并行解析所有相册
let parsed: Vec<crate::models::parse::ParseResult> = albums.par_iter()
.filter_map(|album| {
let title = album.title.as_deref()?;
if title.is_empty() { return None; }
Some(title_parser::parse_album_title(&album.id, title))
})
.collect();
// 单事务批量写入 search.db
match search_db.batch_sync_parse_results(&parsed).await {
Ok(n) => {
println!("[Sync->search.db] 全量同步完成:{} 个相册", n);
n
}
// ── 2. 写入 persons → album_persons ──
search_db.clear_album_persons(&album.id).await.ok();
if let Some(title) = album.title.as_deref() {
if !title.is_empty() {
let parse_result = title_parser::parse_album_title(&album.id, title);
for person_name in &parse_result.person_names {
let person_id = match person_cache.get(person_name) {
Some(&id) => id,
None => {
match search_db.insert_person(person_name, None).await {
Ok(id) => { person_cache.insert(person_name.clone(), id); id }
Err(_) => continue,
}
}
};
search_db.insert_album_person(&album.id, person_id).await.ok();
// 同步别名
let (primary, aliases) = title_parser::split_person_name_with_aliases(person_name);
if !aliases.is_empty() {
for (alias_type, alias_name) in &aliases {
if alias_name != person_name {
search_db.insert_person_alias(person_id, alias_type, alias_name).await.ok();
}
}
if primary != person_name.as_str() && !primary.is_empty() {
search_db.insert_person_alias(person_id, "primary", &primary).await.ok();
}
}
}
}
Err(e) => {
eprintln!("[Sync->search.db] 批量同步失败: {}", e);
0
}
synced += 1;
}
// ── 3. 更新 person counts(从 search.db 的 album_persons 直接计数,两库 person_id 各自独立)──
for (person_name, person_id) in &person_cache {
let albums_count: i64 = match sqlx::query_scalar::<_, i64>(
"SELECT COUNT(*) FROM album_persons WHERE person_id = ?"
).bind(*person_id).fetch_one(&search_db.pool).await {
Ok(c) => c,
Err(_) => continue,
};
search_db.update_person_counts(*person_id, albums_count, 0).await.ok();
}
println!("[Sync->search.db] 同步完成:{} 个相册,{} 个人物,{} 个标签",
synced, person_cache.len(), tag_cache.len());
synced
}
/// 增量同步:只处理脏集中的相册 ID,按需查询后写入 search.db 的关联表
/// 增量同步:只处理脏集中的相册 ID(单事务批量)
pub async fn sync_dirty_albums(db: &Database, search_db: &SearchDb, dirty: &DirtySet) -> usize {
if dirty.is_empty() {
println!("[Sync->search.db·增量] 脏集为空,跳过");
@@ -446,80 +397,24 @@ pub async fn sync_dirty_albums(db: &Database, search_db: &SearchDb, dirty: &Dirt
let dirty_ids: Vec<String> = dirty.clone().into_inner().into_iter().collect();
let albums = db.get_albums_by_ids(&dirty_ids).await;
let mut synced = 0usize;
let mut tag_cache: std::collections::HashMap<String, i64> = std::collections::HashMap::new();
let mut person_cache: std::collections::HashMap<String, i64> = std::collections::HashMap::new();
// 并行解析脏集相册
let parsed: Vec<crate::models::parse::ParseResult> = albums.par_iter()
.filter_map(|album| {
let title = album.title.as_deref()?;
if title.is_empty() { return None; }
Some(title_parser::parse_album_title(&album.id, title))
})
.collect();
for album in &albums {
// ── 1. 写入 tags → album_tags ──
if let Some(title) = album.title.as_deref() {
if !title.is_empty() {
let parse_result = title_parser::parse_album_title(&album.id, title);
if !parse_result.tags.is_empty() {
search_db.clear_album_tags(&album.id).await.ok();
for (tag_name, category) in &parse_result.tags {
let tag_id = match tag_cache.get(tag_name) {
Some(&id) => id,
None => {
match search_db.insert_tag(tag_name, Some(category)).await {
Ok(id) => { tag_cache.insert(tag_name.clone(), id); id }
Err(_) => continue,
}
}
};
search_db.insert_album_tag(&album.id, tag_id).await.ok();
}
}
}
// 单事务批量写入 search.db
match search_db.batch_sync_parse_results(&parsed).await {
Ok(n) => {
println!("[Sync->search.db·增量] 脏集同步完成:{} 个相册", n);
n
}
// ── 2. 写入 persons → album_persons ──
search_db.clear_album_persons(&album.id).await.ok();
if let Some(title) = album.title.as_deref() {
if !title.is_empty() {
let parse_result = title_parser::parse_album_title(&album.id, title);
for person_name in &parse_result.person_names {
let person_id = match person_cache.get(person_name) {
Some(&id) => id,
None => {
match search_db.insert_person(person_name, None).await {
Ok(id) => { person_cache.insert(person_name.clone(), id); id }
Err(_) => continue,
}
}
};
search_db.insert_album_person(&album.id, person_id).await.ok();
// 同步别名
let (primary, aliases) = title_parser::split_person_name_with_aliases(person_name);
if !aliases.is_empty() {
for (alias_type, alias_name) in &aliases {
if alias_name != person_name {
search_db.insert_person_alias(person_id, alias_type, alias_name).await.ok();
}
}
if primary != person_name.as_str() && !primary.is_empty() {
search_db.insert_person_alias(person_id, "primary", &primary).await.ok();
}
}
}
}
Err(e) => {
eprintln!("[Sync->search.db·增量] 批量同步失败: {}", e);
0
}
synced += 1;
}
// ── 3. 更新 person counts(从 search.db 的 album_persons 计数)──
for (person_name, person_id) in &person_cache {
let albums_count: i64 = match sqlx::query_scalar::<_, i64>(
"SELECT COUNT(*) FROM album_persons WHERE person_id = ?"
).bind(*person_id).fetch_one(&search_db.pool).await {
Ok(c) => c,
Err(_) => continue,
};
search_db.update_person_counts(*person_id, albums_count, 0).await.ok();
}
println!("[Sync->search.db·增量] 同步完成:{} 个相册,{} 个人物,{} 个标签",
synced, person_cache.len(), tag_cache.len());
synced
}

View File

@@ -197,7 +197,7 @@ impl RecommendEngine {
for rp in &mut results {
if let Some(person) = search_db.get_person_by_name(&rp.name).await {
let top_tags = search_db.get_person_top_tags(person.id, 5).await;
let top_tags = search_db.get_person_top_tags(&person.id, 5).await;
rp.top_tags = top_tags.into_iter().map(|(t, _)| t).collect();
}
}

View File

@@ -135,9 +135,9 @@ async fn main() -> std::io::Result<()> {
let dirty = engine::pipeline::phase_collect_new_albums(&boot_db).await;
let new_count = dirty.len();
if new_count > 0 {
info!("[启动] 发现 {} 个新相册,同步到 search.db...", new_count);
info!("[启动] 发现 {} 个新相册,同步 tags/persons 到 search.db...", new_count);
let synced = engine::pipeline::sync_dirty_albums(&boot_db, &boot_search_db, &dirty).await;
info!("[启动] 增量同步完成:{} 个相册(仅 tags/persons)", synced);
info!("[启动] 增量同步完成:{} 个相册(仅 tags/persons,不同步 albums 表)", synced);
} else {
info!("[启动] 没有新相册");
}
@@ -169,10 +169,8 @@ 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());
info!("[定时] 发现 {} 个新相册,增量解析(同时写入 imager.db + search.db)...", new_albums.len());
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

@@ -3,7 +3,7 @@ use super::album::AlbumInfo;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Person {
pub id: i64,
pub id: String,
pub name: String,
pub display_name: Option<String>,
pub total_albums: i64,
@@ -16,7 +16,7 @@ pub struct Person {
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PersonAlias {
pub id: i64,
pub person_id: i64,
pub person_id: String,
pub alias_type: String,
pub alias_name: String,
pub created_at: String,
@@ -31,7 +31,7 @@ pub struct PersonWithAlbums {
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PersonWithCounts {
pub id: i64,
pub id: String,
pub name: String,
pub display_name: Option<String>,
pub total_albums: i64,

View File

@@ -54,7 +54,7 @@ pub async fn get_person_detail(
match p {
Some(p) => {
// 从 search.db 获取相册 ID 列表
let album_ids = search_db.get_person_album_ids(p.id).await;
let album_ids = search_db.get_person_album_ids(&p.id).await;
// 从 imager.db 查询相册详情
let albums = if album_ids.is_empty() {
Vec::new()
@@ -106,8 +106,8 @@ pub async fn get_person_detail(
}
/// GET /api/people/{id}/aliases — 从 search.db 查别名
pub async fn get_person_aliases(search_db: web::Data<Arc<SearchDb>>, path: web::Path<i64>) -> impl Responder {
let aliases = search_db.get_person_aliases(path.into_inner()).await;
pub async fn get_person_aliases(search_db: web::Data<Arc<SearchDb>>, path: web::Path<String>) -> impl Responder {
let aliases = search_db.get_person_aliases(&path.into_inner()).await;
HttpResponse::Ok().json(aliases)
}
@@ -137,8 +137,11 @@ pub async fn delete_person(
};
// 2. 从 imager.db 删除(persons + person_aliases + person_albums)
// person.id 来自 imager.db,存储为 i64,模型层转为 String,这里转回 i64
if let Some(person) = db.get_person_by_name(&person_name).await {
db.delete_person_completely(person.id).await;
if let Ok(pid) = person.id.parse::<i64>() {
db.delete_person_completely(pid).await;
}
}
// 3. 重置受影响相册的 processed_at(触发重新解析)
@@ -155,7 +158,6 @@ pub async fn delete_person(
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, &search_db, &dirty).await;
let synced = engine::pipeline::sync_dirty_albums(&db, &search_db, &dirty).await;
// 5. 重载内存词典
engine::tag_dict::reload_from_search_db(&search_db).await;
@@ -169,10 +171,9 @@ pub async fn delete_person(
"albums_affected": album_ids.len(),
"persons_updated": persons_updated,
"tags_updated": tags_updated,
"synced": synced,
"message": format!(
"人物 '{}' 已删除,影响 {} 个相册,重新解析完成(更新人物 {},更新标签 {},同步 {} 个)",
person_name, album_ids.len(), persons_updated, tags_updated, synced,
"人物 '{}' 已删除,影响 {} 个相册,重新解析完成(更新人物 {},更新标签 {})",
person_name, album_ids.len(), persons_updated, tags_updated,
)
}))
} else {

View File

@@ -86,18 +86,15 @@ pub async fn delete_tag(
.ok();
}
// 3. 重新解析受影响相册
// 3. 重新解析受影响相册(同时写入 imager.db + search.db)
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, &search_db, &dirty).await;
// 4. 同步到 search.db
let synced = engine::pipeline::sync_dirty_albums(&db, &search_db, &dirty).await;
// 5. 重载内存词典(dict 表已更新)
// 4. 重载内存词典(dict 表已更新)
engine::tag_dict::reload_from_search_db(&search_db).await;
// 6. 刷新内存缓存
// 5. 刷新内存缓存
crate::cache::refresh_all_caches(&db, &search_db, &albums_cache, &persons_cache, &tags_cache).await;
HttpResponse::Ok().json(serde_json::json!({
@@ -106,10 +103,9 @@ pub async fn delete_tag(
"albums_affected": album_ids.len(),
"persons_updated": persons_updated,
"tags_updated": tags_updated,
"synced": synced,
"message": format!(
"标签 '{}' 已删除,影响 {} 个相册,重新解析完成(更新人物 {},更新标签 {},同步 {} 个)",
tag_name, album_ids.len(), persons_updated, tags_updated, synced,
"标签 '{}' 已删除,影响 {} 个相册,重新解析完成(更新人物 {},更新标签 {})",
tag_name, album_ids.len(), persons_updated, tags_updated,
)
}))
}

View File

@@ -3,6 +3,25 @@ mod search;
pub use search::*;
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions, Row};
use uuid::Uuid;
/// 从 name 计算确定性 tag UUID(无需查数据库即可得到 id)
pub fn tag_uuid(name: &str) -> String {
Uuid::new_v5(
&Uuid::NAMESPACE_URL,
format!("https://imager.dev/tag/{}", name).as_bytes(),
)
.to_string()
}
/// 从 name 计算确定性 person UUID(无需查数据库即可得到 id)
pub fn person_uuid(name: &str) -> String {
Uuid::new_v5(
&Uuid::NAMESPACE_URL,
format!("https://imager.dev/person/{}", name).as_bytes(),
)
.to_string()
}
#[derive(Debug, Clone)]
pub struct DictEntry {
@@ -40,14 +59,25 @@ impl SearchDb {
sqlx::query("PRAGMA busy_timeout=5000;").execute(&pool).await?;
sqlx::query("PRAGMA wal_autocheckpoint=1000;").execute(&pool).await?;
sqlx::query("PRAGMA journal_size_limit=67108864;").execute(&pool).await?;
sqlx::query("PRAGMA cache_size=-64000;").execute(&pool).await?;
sqlx::query("PRAGMA cache_size=-256000;").execute(&pool).await?;
sqlx::query("PRAGMA temp_store=MEMORY;").execute(&pool).await?;
sqlx::query("PRAGMA mmap_size=268435456;").execute(&pool).await?;
Self::create_tables(&pool).await?;
Ok(Self { pool })
}
/// 在批量写入前临时调低 synchronous 级别提升性能
pub async fn set_sync_off(&self) {
let _ = sqlx::query("PRAGMA synchronous=OFF;").execute(&self.pool).await;
}
/// 批量写入后恢复 synchronous 级别
pub async fn set_sync_normal(&self) {
let _ = sqlx::query("PRAGMA synchronous=NORMAL;").execute(&self.pool).await;
}
/// 执行 WAL checkpoint(TRUNCATE),将 WAL 完全合并回主文件并清理 -wal / -shm 文件
///
/// 在启动时调用,确保 search.db 文件干净,避免下次启动时 WAL 恢复开销。
@@ -61,18 +91,42 @@ impl SearchDb {
}
async fn create_tables(pool: &SqlitePool) -> Result<(), sqlx::Error> {
// ── 迁移检测:如果 tags 表存在且 id 类型是 INTEGER(旧版),重建表 ──
let has_old_schema: bool = sqlx::query(
"SELECT type FROM pragma_table_info('tags') WHERE name = 'id'"
).fetch_optional(pool).await.ok()
.flatten()
.map(|r| {
let t: String = r.get("type");
// 旧版用 INTEGER,新版用 TEXT
t.contains("INTEGER")
})
.unwrap_or(false);
if has_old_schema {
println!("[search.db] 检测到旧版 INTEGER ID 模式,重建表为 TEXT UUID 模式...");
sqlx::query("DROP TABLE IF EXISTS album_tags").execute(pool).await?;
sqlx::query("DROP TABLE IF EXISTS album_persons").execute(pool).await?;
sqlx::query("DROP TABLE IF EXISTS person_aliases").execute(pool).await?;
sqlx::query("DROP TABLE IF EXISTS tags").execute(pool).await?;
sqlx::query("DROP TABLE IF EXISTS persons").execute(pool).await?;
println!("[search.db] 旧表已删除,即将重建");
}
// ── 标签表(TEXT UUID 主键) ──
sqlx::query(r#"
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
category TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
"#).execute(pool).await?;
// ── 人物表(TEXT UUID 主键) ──
sqlx::query(r#"
CREATE TABLE IF NOT EXISTS persons (
id INTEGER PRIMARY KEY AUTOINCREMENT,
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
display_name TEXT,
total_albums INTEGER DEFAULT 0,
@@ -82,10 +136,11 @@ impl SearchDb {
)
"#).execute(pool).await?;
// ── 人物别名 ──
sqlx::query(r#"
CREATE TABLE IF NOT EXISTS person_aliases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
person_id INTEGER NOT NULL,
person_id TEXT NOT NULL,
alias_type TEXT NOT NULL,
alias_name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
@@ -93,25 +148,27 @@ impl SearchDb {
)
"#).execute(pool).await?;
// ── 相册-标签关联 ──
sqlx::query(r#"
CREATE TABLE IF NOT EXISTS album_tags (
album_id TEXT NOT NULL,
tag_id INTEGER NOT NULL,
tag_id TEXT NOT NULL,
PRIMARY KEY (album_id, tag_id),
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
)
"#).execute(pool).await?;
// ── 相册-人物关联 ──
sqlx::query(r#"
CREATE TABLE IF NOT EXISTS album_persons (
album_id TEXT NOT NULL,
person_id INTEGER NOT NULL,
person_id TEXT NOT NULL,
PRIMARY KEY (album_id, person_id),
FOREIGN KEY (person_id) REFERENCES persons(id) ON DELETE CASCADE
)
"#).execute(pool).await?;
// ── 新增 albums 表:搜索返回需要的相册基础信息 ──
// ── albums 表:搜索返回需要的相册基础信息 ──
sqlx::query(r#"
CREATE TABLE IF NOT EXISTS albums (
id TEXT PRIMARY KEY,
@@ -122,6 +179,7 @@ impl SearchDb {
)
"#).execute(pool).await?;
// ── dict 表 ──
sqlx::query(r#"
CREATE TABLE IF NOT EXISTS dict (
word TEXT NOT NULL PRIMARY KEY,
@@ -133,6 +191,7 @@ impl SearchDb {
)
"#).execute(pool).await?;
// ── 索引 ──
sqlx::query("CREATE INDEX IF NOT EXISTS idx_albums_title ON albums(title)").execute(pool).await?;
sqlx::query("CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name)").execute(pool).await?;
sqlx::query("CREATE INDEX IF NOT EXISTS idx_persons_name ON persons(name)").execute(pool).await?;
@@ -177,56 +236,32 @@ impl SearchDb {
Ok(())
}
pub async fn insert_tag(&self, name: &str, category: Option<&str>) -> Result<i64, sqlx::Error> {
let result = sqlx::query(r#"
INSERT OR IGNORE INTO tags (name, category) VALUES (?, ?)
"#)
.bind(name)
.bind(category)
.execute(&self.pool)
.await?;
let id = result.last_insert_rowid();
if id == 0 {
let found_id: i64 = sqlx::query_scalar("SELECT id FROM tags WHERE name = ?")
.bind(name)
.fetch_one(&self.pool)
.await?;
Ok(found_id)
} else {
Ok(id)
}
/// 插入标签(使用确定性 UUID,无需 SELECT 回查)
pub async fn insert_tag(&self, name: &str, category: Option<&str>) -> Result<String, sqlx::Error> {
let id = tag_uuid(name);
sqlx::query(r#"INSERT OR IGNORE INTO tags (id, name, category) VALUES (?, ?, ?)"#)
.bind(&id).bind(name).bind(category)
.execute(&self.pool).await?;
Ok(id)
}
pub async fn insert_person(&self, name: &str, display_name: Option<&str>) -> Result<i64, sqlx::Error> {
let result = sqlx::query(r#"
INSERT OR IGNORE INTO persons (name, display_name) VALUES (?, ?)
"#)
.bind(name)
.bind(display_name)
.execute(&self.pool)
.await?;
let id = result.last_insert_rowid();
if id == 0 {
let found_id: i64 = sqlx::query_scalar("SELECT id FROM persons WHERE name = ?")
.bind(name)
.fetch_one(&self.pool)
.await?;
Ok(found_id)
} else {
Ok(id)
}
/// 插入人物(使用确定性 UUID,无需 SELECT 回查)
pub async fn insert_person(&self, name: &str, display_name: Option<&str>) -> Result<String, sqlx::Error> {
let id = person_uuid(name);
sqlx::query(r#"INSERT OR IGNORE INTO persons (id, name, display_name) VALUES (?, ?, ?)"#)
.bind(&id).bind(name).bind(display_name)
.execute(&self.pool).await?;
Ok(id)
}
pub async fn update_person_counts(&self, person_id: i64, total_albums: i64, total_images: i64) -> Result<(), sqlx::Error> {
pub async fn update_person_counts(&self, person_id: &str, total_albums: i64, total_images: i64) -> Result<(), sqlx::Error> {
sqlx::query("UPDATE persons SET total_albums = ?, total_images = ?, updated_at = datetime('now') WHERE id = ?")
.bind(total_albums).bind(total_images).bind(person_id)
.execute(&self.pool).await?;
Ok(())
}
pub async fn insert_person_alias(&self, person_id: i64, alias_type: &str, alias_name: &str) -> Result<(), sqlx::Error> {
pub async fn insert_person_alias(&self, person_id: &str, alias_type: &str, alias_name: &str) -> Result<(), sqlx::Error> {
sqlx::query(r#"
INSERT OR IGNORE INTO person_aliases (person_id, alias_type, alias_name) VALUES (?, ?, ?)
"#)
@@ -238,7 +273,7 @@ impl SearchDb {
Ok(())
}
pub async fn insert_album_tag(&self, album_id: &str, tag_id: i64) -> Result<(), sqlx::Error> {
pub async fn insert_album_tag(&self, album_id: &str, tag_id: &str) -> Result<(), sqlx::Error> {
sqlx::query(r#"
INSERT OR IGNORE INTO album_tags (album_id, tag_id) VALUES (?, ?)
"#)
@@ -249,7 +284,7 @@ impl SearchDb {
Ok(())
}
pub async fn insert_album_person(&self, album_id: &str, person_id: i64) -> Result<(), sqlx::Error> {
pub async fn insert_album_person(&self, album_id: &str, person_id: &str) -> Result<(), sqlx::Error> {
sqlx::query(r#"
INSERT OR IGNORE INTO album_persons (album_id, person_id) VALUES (?, ?)
"#)
@@ -317,34 +352,20 @@ impl SearchDb {
/// 删除 tag:从 dict/album_tags/tags 表中删除,返回受影响的 album_id 列表
pub async fn delete_tag_and_get_albums(&self, tag_name: &str) -> Result<Vec<String>, sqlx::Error> {
// 1. 查找 tag_id(tags 表中可能存在多个同名 tag,但 name 应唯一)
let tag_ids: Vec<i64> = sqlx::query_scalar("SELECT id FROM tags WHERE name = ?")
.bind(tag_name).fetch_all(&self.pool).await.unwrap_or_default();
// 1. 计算确定性 tag UUID
let tag_id = tag_uuid(tag_name);
// 2. 查找关联 album_ids
let album_ids: Vec<String> = if tag_ids.is_empty() {
Vec::new()
} else {
// 用参数占位构建 IN 查询
let placeholders: Vec<String> = tag_ids.iter().map(|_| "?".to_string()).collect();
let sql = format!("SELECT DISTINCT album_id FROM album_tags WHERE tag_id IN ({})", placeholders.join(","));
let mut q = sqlx::query(&sql);
for id in &tag_ids { q = q.bind(*id); }
q.fetch_all(&self.pool).await?
.iter().map(|r| r.get::<String, _>("album_id")).collect()
};
let album_ids: Vec<String> = sqlx::query_scalar::<_, String>(
"SELECT DISTINCT album_id FROM album_tags WHERE tag_id = ?"
).bind(&tag_id).fetch_all(&self.pool).await.unwrap_or_default();
// 3. 删除 album_tags 关联
if !tag_ids.is_empty() {
let placeholders: Vec<String> = tag_ids.iter().map(|_| "?".to_string()).collect();
let sql = format!("DELETE FROM album_tags WHERE tag_id IN ({})", placeholders.join(","));
let mut q = sqlx::query(&sql);
for id in &tag_ids { q = q.bind(*id); }
q.execute(&self.pool).await?;
}
sqlx::query("DELETE FROM album_tags WHERE tag_id = ?")
.bind(&tag_id).execute(&self.pool).await?;
// 4. 删除 tags 表条目
sqlx::query("DELETE FROM tags WHERE name = ?").bind(tag_name).execute(&self.pool).await?;
sqlx::query("DELETE FROM tags WHERE id = ?").bind(&tag_id).execute(&self.pool).await?;
// 5. 删除 dict 表条目
sqlx::query("DELETE FROM dict WHERE word = ? AND type = 'tag'").bind(tag_name).execute(&self.pool).await?;
@@ -354,40 +375,21 @@ impl SearchDb {
/// 删除 person:从 search.db 的 album_persons/persons/person_aliases 中删除,返回受影响的 album_id 列表
pub async fn delete_person_and_get_albums(&self, person_name: &str) -> Result<Vec<String>, sqlx::Error> {
// 1. 查找 person_id
let person_ids: Vec<i64> = sqlx::query_scalar("SELECT id FROM persons WHERE name = ?")
.bind(person_name).fetch_all(&self.pool).await.unwrap_or_default();
// 1. 计算确定性 person UUID
let person_id = person_uuid(person_name);
// 2. 查找关联 album_ids
let album_ids: Vec<String> = if person_ids.is_empty() {
Vec::new()
} else {
let placeholders: Vec<String> = person_ids.iter().map(|_| "?".to_string()).collect();
let sql = format!("SELECT DISTINCT album_id FROM album_persons WHERE person_id IN ({})", placeholders.join(","));
let mut q = sqlx::query(&sql);
for id in &person_ids { q = q.bind(*id); }
q.fetch_all(&self.pool).await?
.iter().map(|r| r.get::<String, _>("album_id")).collect()
};
let album_ids: Vec<String> = sqlx::query_scalar::<_, String>(
"SELECT DISTINCT album_id FROM album_persons WHERE person_id = ?"
).bind(&person_id).fetch_all(&self.pool).await.unwrap_or_default();
// 3. 删除关联表
if !person_ids.is_empty() {
let placeholders: Vec<String> = person_ids.iter().map(|_| "?".to_string()).collect();
let sql = format!("DELETE FROM album_persons WHERE person_id IN ({})", placeholders.join(","));
let mut q = sqlx::query(&sql);
for id in &person_ids { q = q.bind(*id); }
q.execute(&self.pool).await?;
let sql2 = format!("DELETE FROM person_aliases WHERE person_id IN ({})", placeholders.join(","));
let mut q2 = sqlx::query(&sql2);
for id in &person_ids { q2 = q2.bind(*id); }
q2.execute(&self.pool).await?;
let sql3 = format!("DELETE FROM persons WHERE id IN ({})", placeholders.join(","));
let mut q3 = sqlx::query(&sql3);
for id in &person_ids { q3 = q3.bind(*id); }
q3.execute(&self.pool).await?;
}
sqlx::query("DELETE FROM album_persons WHERE person_id = ?")
.bind(&person_id).execute(&self.pool).await?;
sqlx::query("DELETE FROM person_aliases WHERE person_id = ?")
.bind(&person_id).execute(&self.pool).await?;
sqlx::query("DELETE FROM persons WHERE id = ?")
.bind(&person_id).execute(&self.pool).await?;
// 4. 删除 dict 表中的对应条目(如果存在)
sqlx::query("DELETE FROM dict WHERE word = ? AND type = 'person'")
@@ -396,6 +398,142 @@ impl SearchDb {
Ok(album_ids)
}
/// 批量同步解析结果到 search.db(单事务 + 预计算 UUID + 批量操作)
///
/// 核心优化:
/// 1. 预加载全部 tags/persons 到 HashMap(仅 2 次 SELECT)
/// 2. 预计算所有 tag/person 的 UUID(零数据库查询得到 ID)
/// 3. 全量 `DELETE WHERE album_id IN (...)` 代替逐条 DELETE
/// 4. 逐条 INSERT 但无需 SELECT 查 ID(UUID 直接写入)
/// 5. 每事务一次 person counts 刷新
pub async fn batch_sync_parse_results(&self, results: &[crate::models::parse::ParseResult]) -> Result<usize, sqlx::Error> {
use std::collections::HashMap;
let mut tx = self.pool.begin().await?;
// 1. 预加载现有 tags/persons(仅 2 次 SELECT)
let existing_tags: HashMap<String, String> = sqlx::query_as::<_, (String, String)>(
"SELECT name, id FROM tags"
).fetch_all(&mut *tx).await?.into_iter().collect();
let existing_persons: HashMap<String, String> = sqlx::query_as::<_, (String, String)>(
"SELECT name, id FROM persons"
).fetch_all(&mut *tx).await?.into_iter().collect();
// 2. 收集所有相册 ID,用于批量 DELETE
let album_ids: Vec<&str> = results.iter().map(|r| r.album_id.as_str()).collect();
if !album_ids.is_empty() {
let ph: Vec<String> = album_ids.iter().map(|_| "?".to_string()).collect();
let sql = format!("DELETE FROM album_tags WHERE album_id IN ({})", ph.join(","));
let mut q = sqlx::query(&sql);
for id in &album_ids { q = q.bind(id); }
q.execute(&mut *tx).await?;
let ph2: Vec<String> = album_ids.iter().map(|_| "?".to_string()).collect();
let sql2 = format!("DELETE FROM album_persons WHERE album_id IN ({})", ph2.join(","));
let mut q2 = sqlx::query(&sql2);
for id in &album_ids { q2 = q2.bind(id); }
q2.execute(&mut *tx).await?;
}
// 3. 收集去重后的 tag 和 person 名称
let mut unique_tags: Vec<(String, Option<String>)> = Vec::new();
let mut seen_tags = std::collections::HashSet::new();
let mut unique_persons: Vec<String> = Vec::new();
let mut seen_persons = std::collections::HashSet::new();
for result in results {
for (tag_name, category) in &result.tags {
if seen_tags.insert(tag_name.clone()) {
unique_tags.push((tag_name.clone(), Some(category.clone())));
}
}
for person_name in &result.person_names {
if seen_persons.insert(person_name.clone()) {
unique_persons.push(person_name.clone());
}
}
}
// 4a. 写入标签:对不存在的标签 INSERT(使用预计算 UUID)
for (tag_name, category) in &unique_tags {
if !existing_tags.contains_key(tag_name) {
let id = tag_uuid(tag_name);
sqlx::query("INSERT OR IGNORE INTO tags (id, name, category) VALUES (?, ?, ?)")
.bind(&id).bind(tag_name).bind(category)
.execute(&mut *tx).await?;
}
}
// 4b. 写入人物:对不存在的人物 INSERT(使用预计算 UUID)
for person_name in &unique_persons {
if !existing_persons.contains_key(person_name) {
let id = person_uuid(person_name);
sqlx::query("INSERT OR IGNORE INTO persons (id, name) VALUES (?, ?)")
.bind(&id).bind(person_name)
.execute(&mut *tx).await?;
}
}
// 5. 写入批量关联数据(无 SELECT 查找,直接使用预计算的 UUID)
let mut tag_id_cache: HashMap<String, String> = existing_tags.clone();
let mut person_id_cache: HashMap<String, String> = existing_persons.clone();
for result in results {
let album_id = &result.album_id;
// ── album_tags ──
for (tag_name, _category) in &result.tags {
let tid = tag_id_cache.get(tag_name.as_str())
.cloned()
.unwrap_or_else(|| tag_uuid(tag_name));
// 缓存新写入的 tag(即使上一个缓存中已更新,这里确保缓存完整)
tag_id_cache.entry(tag_name.clone()).or_insert_with(|| tid.clone());
sqlx::query("INSERT OR IGNORE INTO album_tags (album_id, tag_id) VALUES (?, ?)")
.bind(album_id).bind(&tid).execute(&mut *tx).await?;
}
// ── album_persons ──
for person_name in &result.person_names {
let pid = person_id_cache.get(person_name.as_str())
.cloned()
.unwrap_or_else(|| person_uuid(person_name));
person_id_cache.entry(person_name.clone()).or_insert_with(|| pid.clone());
sqlx::query("INSERT OR IGNORE INTO album_persons (album_id, person_id) VALUES (?, ?)")
.bind(album_id).bind(&pid).execute(&mut *tx).await?;
// 别名写入
let (primary, aliases) = crate::engine::title_parser::split_person_name_with_aliases(person_name);
if !aliases.is_empty() {
for (alias_type, alias_name) in &aliases {
if alias_name != person_name {
sqlx::query("INSERT OR IGNORE INTO person_aliases (person_id, alias_type, alias_name) VALUES (?, ?, ?)")
.bind(&pid).bind(alias_type).bind(alias_name).execute(&mut *tx).await?;
}
}
if primary != person_name.as_str() && !primary.is_empty() {
sqlx::query("INSERT OR IGNORE INTO person_aliases (person_id, alias_type, alias_name) VALUES (?, ?, ?)")
.bind(&pid).bind("primary").bind(&primary).execute(&mut *tx).await?;
}
}
}
}
// 6. 更新 person counts(一次批量)
for (_name, pid) in &person_id_cache {
let count: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM album_persons WHERE person_id = ?"
).bind(pid).fetch_one(&mut *tx).await.unwrap_or(0);
sqlx::query("UPDATE persons SET total_albums = ?, total_images = 0 WHERE id = ?")
.bind(count).bind(pid).execute(&mut *tx).await?;
}
tx.commit().await?;
let total = results.len();
println!("[batch_sync] 单事务同步完成:{} 个相册,{} 个标签(key),{} 个人物(key)",
total, tag_id_cache.len(), person_id_cache.len());
Ok(total)
}
pub async fn clear_all(&self) -> Result<(), sqlx::Error> {
let mut tx = self.pool.begin().await?;
sqlx::query("DELETE FROM album_tags").execute(&mut *tx).await?;

View File

@@ -212,7 +212,7 @@ impl SearchDb {
// ═══════════════════════════════════════
/// 获取人物别名列表
pub async fn get_person_aliases(&self, person_id: i64) -> Vec<PersonAlias> {
pub async fn get_person_aliases(&self, person_id: &str) -> Vec<PersonAlias> {
let rows = sqlx::query(
"SELECT id, person_id, alias_type, alias_name, created_at FROM person_aliases WHERE person_id = ?"
).bind(person_id).fetch_all(&self.pool).await.unwrap_or_default();
@@ -234,11 +234,11 @@ impl SearchDb {
let mut results = Vec::new();
for r in &persons {
let id: i64 = r.get("id");
let id: String = r.get("id");
let name: String = r.get("name");
let aliases: Vec<String> = sqlx::query_scalar(
"SELECT alias_name FROM person_aliases WHERE person_id = ? ORDER BY alias_type"
).bind(id).fetch_all(&self.pool).await.unwrap_or_default();
).bind(&id).fetch_all(&self.pool).await.unwrap_or_default();
results.push(PersonWithCounts {
id,
@@ -258,10 +258,10 @@ impl SearchDb {
"SELECT id, name, display_name, total_albums, total_images FROM persons WHERE name = ?"
).bind(name).fetch_optional(&self.pool).await.ok()??;
let id: i64 = row.get("id");
let id: String = row.get("id");
let aliases: Vec<String> = sqlx::query_scalar(
"SELECT alias_name FROM person_aliases WHERE person_id = ? ORDER BY alias_type"
).bind(id).fetch_all(&self.pool).await.unwrap_or_default();
).bind(&id).fetch_all(&self.pool).await.unwrap_or_default();
Some(PersonWithCounts {
id,
@@ -274,14 +274,14 @@ impl SearchDb {
}
/// 获取人物的所有相册 ID(从 search.db 的 album_persons 关联表查询)
pub async fn get_person_album_ids(&self, person_id: i64) -> Vec<String> {
pub async fn get_person_album_ids(&self, person_id: &str) -> Vec<String> {
sqlx::query_scalar::<_, String>(
"SELECT album_id FROM album_persons WHERE person_id = ?"
).bind(person_id).fetch_all(&self.pool).await.unwrap_or_default()
}
/// 获取人物的 top 标签(用于详情页)
pub async fn get_person_top_tags(&self, person_id: i64, limit: usize) -> Vec<(String, i64)> {
pub async fn get_person_top_tags(&self, person_id: &str, limit: usize) -> Vec<(String, i64)> {
let rows = sqlx::query(
r#"SELECT t.name, COUNT(*) as cnt
FROM tags t
@@ -313,7 +313,7 @@ impl SearchDb {
|| name_lower.contains(&q)
|| alias_matches;
if matched && seen.insert(p.id) {
if matched && seen.insert(p.id.clone()) {
results.push(p.clone());
}
}

View File

@@ -64,8 +64,8 @@ pub async fn export_to_search_db(
let albums = source_db.get_all_albums().await.map_err(|e| format!("读取相册列表失败: {}", e))?;
println!("[搜索导出] 共读取 {} 个相册(来自 imager.db)", albums.len());
let mut tag_cache: HashMap<String, i64> = HashMap::new();
let mut person_cache: HashMap<String, i64> = HashMap::new();
let mut tag_cache: HashMap<String, String> = HashMap::new();
let mut person_cache: HashMap<String, String> = HashMap::new();
let mut result = SearchExportResult::default();
for album in &albums {
@@ -97,21 +97,22 @@ pub async fn export_to_search_db(
result.dict_matched_tags += 1;
}
// 使用确定性 UUID,无需查数据库得到 id
let tag_id = match tag_cache.get(tag_name) {
Some(&id) => id,
Some(id) => id.clone(),
None => {
let id = search_db
.insert_tag(tag_name, Some(category))
.await
.map_err(|e| format!("插入标签 {} 失败: {}", tag_name, e))?;
tag_cache.insert(tag_name.clone(), id);
tag_cache.insert(tag_name.clone(), id.clone());
result.tags_created += 1;
id
}
};
search_db
.insert_album_tag(&album_id, tag_id)
.insert_album_tag(&album_id, &tag_id)
.await
.map_err(|e| format!("关联相册标签 {} → {} 失败: {}", album_id, tag_name, e))?;
result.album_tags_created += 1;
@@ -127,21 +128,22 @@ pub async fn export_to_search_db(
result.dict_resolved_aliases += 1;
}
// 使用确定性 UUID,无需查数据库得到 id
let person_id = match person_cache.get(person_name) {
Some(&id) => id,
Some(id) => id.clone(),
None => {
let id = search_db
.insert_person(person_name, None)
.await
.map_err(|e| format!("插入人物 {} 失败: {}", person_name, e))?;
person_cache.insert(person_name.clone(), id);
person_cache.insert(person_name.clone(), id.clone());
result.persons_created += 1;
id
}
};
search_db
.insert_album_person(&album_id, person_id)
.insert_album_person(&album_id, &person_id)
.await
.map_err(|e| format!("关联相册人物 {} → {} 失败: {}", album_id, person_name, e))?;
result.album_persons_created += 1;
@@ -152,7 +154,7 @@ pub async fn export_to_search_db(
for (alias_type, alias_name) in &aliases {
if alias_name != person_name {
search_db
.insert_person_alias(person_id, alias_type, alias_name)
.insert_person_alias(&person_id, alias_type, alias_name)
.await
.map_err(|e| format!("插入别名 {} → {} 失败: {}", person_name, alias_name, e))?;
result.aliases_created += 1;
@@ -160,7 +162,7 @@ pub async fn export_to_search_db(
}
if primary != person_name.as_str() && !primary.is_empty() {
search_db
.insert_person_alias(person_id, "primary", &primary)
.insert_person_alias(&person_id, "primary", &primary)
.await
.map_err(|e| format!("插入别名 {} → {} 失败: {}", person_name, primary, e))?;
result.aliases_created += 1;