//! 搜索读取方法 — 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, 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 = 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, 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 { 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> { 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> = 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 { 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 { 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::("cnt") as f64 / total as f64, }).collect() } // ═══════════════════════════════════════ // 人物搜索 // ═══════════════════════════════════════ /// 获取人物别名列表 pub async fn get_person_aliases(&self, person_id: i64) -> Vec { 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 { 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 = 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 { 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 = 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 { 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 { 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, } } }