369 lines
14 KiB
Rust
369 lines
14 KiB
Rust
//! 搜索读取方法 — 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,
|
||
}
|
||
}
|
||
}
|