Files
rust-imager/src/search_db/search.rs
2026-07-21 00:51:55 +08:00

369 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 搜索读取方法 — SearchDb 的只读接口
//!
//! 所有搜索相关的数据读取都从这里进行,完全不碰主库。
use sqlx::Row;
use std::collections::HashMap;
use uuid::Uuid;
use crate::models::*;
use super::SearchDb;
impl SearchDb {
// ═══════════════════════════════════════
// 相册搜索
// ═══════════════════════════════════════
/// 组合搜索:按 tag/person/标题关键词 任意组合过滤(AND)
pub async fn search_albums(
&self,
tag: Option<&str>,
person: Option<&str>,
title_q: Option<&str>,
limit: i64,
offset: i64,
) -> Result<Vec<AlbumInfo>, sqlx::Error> {
let has_tag = tag.map(|s| !s.is_empty()).unwrap_or(false);
let has_person = person.map(|s| !s.is_empty()).unwrap_or(false);
let has_q = title_q.map(|s| !s.is_empty()).unwrap_or(false);
if !has_tag && !has_person && !has_q {
return self.get_all_albums().await;
}
let mut sql = String::from(
"SELECT DISTINCT a.id, a.title, a.cover_image_id, a.image_count, a.created_at
FROM albums a"
);
let mut conditions: Vec<String> = Vec::new();
if has_tag {
sql.push_str(" JOIN album_tags at ON a.id = at.album_id JOIN tags t ON at.tag_id = t.id");
conditions.push("t.name = ?".to_string());
}
if has_person {
sql.push_str(" JOIN album_persons ap ON a.id = ap.album_id JOIN persons p ON ap.person_id = p.id");
conditions.push("(p.name = ? OR LOWER(p.name) LIKE ?)".to_string());
}
if !conditions.is_empty() {
sql.push_str(" WHERE ");
sql.push_str(&conditions.join(" AND "));
}
if has_q {
if conditions.is_empty() {
sql.push_str(" WHERE ");
} else {
sql.push_str(" AND ");
}
sql.push_str("a.title LIKE ?");
}
sql.push_str(" ORDER BY a.created_at DESC LIMIT ? OFFSET ?");
let mut query = sqlx::query(&sql);
if has_tag { query = query.bind(tag.unwrap()); }
if has_person {
let p = person.unwrap();
query = query.bind(p).bind(format!("%{}%", p));
}
if has_q { query = query.bind(format!("%{}%", title_q.unwrap())); }
query = query.bind(limit).bind(offset);
let rows = query.fetch_all(&self.pool).await?;
Ok(rows.iter().map(|r| {
let id_str: String = r.get("id");
AlbumInfo {
id: Uuid::parse_str(&id_str).unwrap_or_else(|_| Uuid::nil()),
title: r.get("title"),
document: String::new(),
description: None,
cover_image_id: r.get("cover_image_id"),
image_count: r.get("image_count"),
created_at: r.get("created_at"),
}
}).collect())
}
/// 获取全量相册列表(缓存烘焙用)
pub async fn get_all_albums(&self) -> Result<Vec<AlbumInfo>, sqlx::Error> {
let rows = sqlx::query(
"SELECT id, title, cover_image_id, image_count, created_at FROM albums ORDER BY created_at DESC"
).fetch_all(&self.pool).await?;
Ok(rows.iter().map(|r| {
let id_str: String = r.get("id");
AlbumInfo {
id: Uuid::parse_str(&id_str).unwrap_or_else(|_| Uuid::nil()),
title: r.get("title"),
document: String::new(),
description: None,
cover_image_id: r.get("cover_image_id"),
image_count: r.get("image_count"),
created_at: r.get("created_at"),
}
}).collect())
}
// ═══════════════════════════════════════
// 标签搜索
// ═══════════════════════════════════════
/// 获取热门标签(按使用次数排序)
pub async fn get_popular_tags(&self, limit: usize) -> Vec<TagStat> {
let limit = limit.min(500);
let rows = sqlx::query(
r#"SELECT t.name, COUNT(at.album_id) as cnt
FROM tags t
JOIN album_tags at ON t.id = at.tag_id
GROUP BY t.id, t.name
ORDER BY cnt DESC LIMIT ?"#
).bind(limit as i64).fetch_all(&self.pool).await.unwrap_or_default();
rows.iter().map(|r| TagStat {
tag: r.get("name"),
count: r.get("cnt"),
album_count: 0,
}).collect()
}
/// 获取标签倒排索引:tag → albums(缓存烘焙用)
pub async fn get_all_tag_albums(&self) -> HashMap<String, Vec<AlbumInfo>> {
let rows = sqlx::query(
r#"SELECT t.name as tag_name,
a.id, a.title, a.cover_image_id, a.image_count, a.created_at
FROM tags t
JOIN album_tags at ON t.id = at.tag_id
JOIN albums a ON a.id = at.album_id
ORDER BY a.created_at DESC"#
).fetch_all(&self.pool).await.unwrap_or_default();
let mut map: HashMap<String, Vec<AlbumInfo>> = HashMap::new();
for r in &rows {
let tag_name: String = r.get("tag_name");
let id_str: String = r.get("id");
let album = AlbumInfo {
id: Uuid::parse_str(&id_str).unwrap_or_else(|_| Uuid::nil()),
title: r.get("title"),
document: String::new(),
description: None,
cover_image_id: r.get("cover_image_id"),
image_count: r.get("image_count"),
created_at: r.get("created_at"),
};
map.entry(tag_name).or_default().push(album);
}
map
}
/// 按标签搜索相册
pub async fn search_albums_by_tag(&self, tag: &str) -> Vec<AlbumInfo> {
let rows = sqlx::query(
r#"SELECT a.id, a.title, a.cover_image_id, a.image_count, a.created_at
FROM albums a
JOIN album_tags at ON a.id = at.album_id
JOIN tags t ON at.tag_id = t.id
WHERE t.name = ?
ORDER BY a.created_at DESC"#
).bind(tag).fetch_all(&self.pool).await.unwrap_or_default();
rows.iter().map(|r| {
let id_str: String = r.get("id");
AlbumInfo {
id: Uuid::parse_str(&id_str).unwrap_or_else(|_| Uuid::nil()),
title: r.get("title"),
document: String::new(),
description: None,
cover_image_id: r.get("cover_image_id"),
image_count: r.get("image_count"),
created_at: r.get("created_at"),
}
}).collect()
}
/// 获取标签共现数据(关联推荐用)
pub async fn get_tag_co_occurrences(&self, tag: &str, limit: i64) -> Vec<TagCoOccurrence> {
let rows = sqlx::query(
r#"SELECT t2.name as co_tag, COUNT(*) as cnt
FROM album_tags at1
JOIN album_tags at2 ON at1.album_id = at2.album_id AND at1.tag_id != at2.tag_id
JOIN tags t1 ON at1.tag_id = t1.id
JOIN tags t2 ON at2.tag_id = t2.id
WHERE t1.name = ?
GROUP BY t2.name
ORDER BY cnt DESC LIMIT ?"#
).bind(tag).bind(limit).fetch_all(&self.pool).await.unwrap_or_default();
let total: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM album_tags at JOIN tags t ON at.tag_id = t.id WHERE t.name = ?"
).bind(tag).fetch_one(&self.pool).await.unwrap_or(1);
rows.iter().map(|r| TagCoOccurrence {
tag: tag.to_string(),
co_tag: r.get("co_tag"),
count: r.get("cnt"),
score: r.get::<i64, _>("cnt") as f64 / total as f64,
}).collect()
}
// ═══════════════════════════════════════
// 人物搜索
// ═══════════════════════════════════════
/// 获取人物别名列表
pub async fn get_person_aliases(&self, person_id: i64) -> 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();
rows.iter().map(|r| PersonAlias {
id: r.get("id"),
person_id: r.get("person_id"),
alias_type: r.get("alias_type"),
alias_name: r.get("alias_name"),
created_at: r.get("created_at"),
}).collect()
}
/// 获取所有人物(含别名、排序)
pub async fn get_all_persons_with_aliases(&self) -> Vec<PersonWithCounts> {
let persons = sqlx::query(
"SELECT id, name, display_name, total_albums, total_images FROM persons ORDER BY total_albums DESC"
).fetch_all(&self.pool).await.unwrap_or_default();
let mut results = Vec::new();
for r in &persons {
let id: i64 = 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();
results.push(PersonWithCounts {
id,
name,
display_name: r.get("display_name"),
total_albums: r.get("total_albums"),
total_images: r.get("total_images"),
aliases,
});
}
results
}
/// 按名称查找人物
pub async fn get_person_by_name(&self, name: &str) -> Option<PersonWithCounts> {
let row = sqlx::query(
"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 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();
Some(PersonWithCounts {
id,
name: row.get("name"),
display_name: row.get("display_name"),
total_albums: row.get("total_albums"),
total_images: row.get("total_images"),
aliases,
})
}
/// 获取人物的所有相册
pub async fn get_person_albums(&self, person_id: i64) -> Vec<AlbumInfo> {
let rows = sqlx::query(
r#"SELECT a.id, a.title, a.cover_image_id, a.image_count, a.created_at
FROM albums a
JOIN album_persons ap ON a.id = ap.album_id
WHERE ap.person_id = ?
ORDER BY a.created_at DESC"#
).bind(person_id).fetch_all(&self.pool).await.unwrap_or_default();
rows.iter().map(|r| {
let id_str: String = r.get("id");
AlbumInfo {
id: Uuid::parse_str(&id_str).unwrap_or_else(|_| Uuid::nil()),
title: r.get("title"),
document: String::new(),
description: None,
cover_image_id: r.get("cover_image_id"),
image_count: r.get("image_count"),
created_at: r.get("created_at"),
}
}).collect()
}
/// 获取人物的 top 标签(用于详情页)
pub async fn get_person_top_tags(&self, person_id: i64, limit: usize) -> Vec<(String, i64)> {
let rows = sqlx::query(
r#"SELECT t.name, COUNT(*) as cnt
FROM tags t
JOIN album_tags at ON t.id = at.tag_id
JOIN album_persons ap ON at.album_id = ap.album_id
WHERE ap.person_id = ?
GROUP BY t.name
ORDER BY cnt DESC LIMIT ?"#
).bind(person_id).bind(limit as i64)
.fetch_all(&self.pool).await.unwrap_or_default();
rows.iter().map(|r| (r.get("name"), r.get("cnt"))).collect()
}
/// 搜索人物(按名称/别名模糊匹配,内存)
pub fn search_persons_inline(persons: &[PersonWithCounts], query: &str) -> Vec<PersonWithCounts> {
let q = query.to_lowercase().trim().to_string();
if q.is_empty() || q.len() < 2 { return Vec::new(); }
let mut seen = std::collections::HashSet::new();
let mut results = Vec::new();
for p in persons {
let name_lower = p.name.to_lowercase();
let alias_matches = p.aliases.iter().any(|a| a.to_lowercase().contains(&q));
let matched = name_lower == q
|| name_lower.starts_with(&q)
|| name_lower.contains(&q)
|| alias_matches;
if matched && seen.insert(p.id) {
results.push(p.clone());
}
}
results.sort_by(|a, b| b.total_albums.cmp(&a.total_albums));
results
}
// ═══════════════════════════════════════
// 元数据统计
// ═══════════════════════════════════════
/// 获取全局统计信息
pub async fn get_metadata_stats(&self) -> MetadataStats {
let total_persons: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM persons")
.fetch_one(&self.pool).await.unwrap_or(0);
let total_aliases: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM person_aliases")
.fetch_one(&self.pool).await.unwrap_or(0);
let total_albums: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM albums")
.fetch_one(&self.pool).await.unwrap_or(0);
let total_images: i64 = sqlx::query_scalar("SELECT COALESCE(SUM(image_count), 0) FROM albums")
.fetch_one(&self.pool).await.unwrap_or(0);
let total_tags: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM album_tags")
.fetch_one(&self.pool).await.unwrap_or(0);
MetadataStats {
total_persons,
total_aliases,
total_albums,
total_images,
total_tags,
}
}
}