use serde::{Deserialize, Serialize}; use uuid::Uuid; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SearchQuery { pub query: String, pub room_id: Option, pub user_id: Option, pub limit: u64, pub offset: u64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SearchResult { pub total: u64, pub hits: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SearchHit { pub message_id: Uuid, pub room_id: Uuid, pub content: String, pub highlighted: String, pub sender_id: Option, pub send_at: chrono::DateTime, pub score: f64, } pub struct SearchEngine { db: db::database::AppDatabase, } impl SearchEngine { pub fn new(db: db::database::AppDatabase) -> Self { Self { db } } pub async fn search( &self, query: SearchQuery, ) -> Result { use sea_orm::*; use models::rooms::room_message; let mut db_query = room_message::Entity::find(); if let Some(room_id) = query.room_id { db_query = db_query.filter(room_message::Column::Room.eq(room_id)); } if let Some(user_id) = query.user_id { db_query = db_query.filter(room_message::Column::SenderId.eq(user_id)); } let escaped_query = query.query .replace('\\', "\\\\") .replace('%', "\\%") .replace('_', "\\_"); let search_term = format!("%{}%", escaped_query); db_query = db_query.filter(room_message::Column::Content.like(&search_term)); let total = db_query.clone().count(&self.db).await .map_err(|_| crate::error::AppTransportError::Internal)?; let messages = db_query .order_by_desc(room_message::Column::SendAt) .limit(query.limit) .offset(query.offset) .all(&self.db) .await .map_err(|_| crate::error::AppTransportError::Internal)?; let hits: Vec = messages .into_iter() .map(|m| SearchHit { message_id: m.id, room_id: m.room, content: m.content.clone(), highlighted: highlight_text(&m.content, &query.query), sender_id: m.sender_id, send_at: m.send_at, score: 1.0, }) .collect(); Ok(SearchResult { total, hits }) } pub async fn index_message( &self, _message_id: Uuid, _content: &str, ) -> Result<(), crate::error::AppTransportError> { Ok(()) } pub async fn delete_message( &self, _message_id: Uuid, ) -> Result<(), crate::error::AppTransportError> { Ok(()) } } fn highlight_text(content: &str, query: &str) -> String { let lower_content = content.to_lowercase(); let lower_query = query.to_lowercase(); if let Some(pos) = lower_content.find(&lower_query) { let before = &content[..pos]; let matched = &content[pos..pos + query.len()]; let after = &content[pos + query.len()..]; format!("{}{}{}", before, matched, after) } else { content.to_string() } }