123 lines
3.3 KiB
Rust
123 lines
3.3 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SearchQuery {
|
|
pub query: String,
|
|
pub room_id: Option<Uuid>,
|
|
pub user_id: Option<Uuid>,
|
|
pub limit: u64,
|
|
pub offset: u64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SearchResult {
|
|
pub total: u64,
|
|
pub hits: Vec<SearchHit>,
|
|
}
|
|
|
|
#[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<Uuid>,
|
|
pub send_at: chrono::DateTime<chrono::Utc>,
|
|
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<SearchResult, crate::error::AppTransportError> {
|
|
use models::rooms::room_message;
|
|
use sea_orm::*;
|
|
|
|
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<SearchHit> = 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!("{}<mark>{}</mark>{}", before, matched, after)
|
|
} else {
|
|
content.to_string()
|
|
}
|
|
}
|