Files
leonwww/search-engine/src/server.rs

196 lines
6.8 KiB
Rust
Raw Normal View History

2025-08-27 20:23:05 +03:00
use anyhow::{Result, Context};
use gurt::prelude::*;
use gurt::GurtError;
use serde_json::json;
use std::sync::Arc;
use tracing::{info, error};
use crate::config::Config;
use crate::indexer::SearchEngine;
use crate::scheduler::BackgroundScheduler;
pub struct SearchServer {
config: Config,
search_engine: Arc<SearchEngine>,
}
impl SearchServer {
pub async fn new(config: Config) -> Result<Self> {
// Connect to database
sqlx::PgPool::connect(&config.database_url()).await
.context("Failed to connect to database")?;
let search_engine = Arc::new(SearchEngine::new(config.clone())?);
Ok(Self {
config,
search_engine,
})
}
pub async fn run(self) -> Result<()> {
info!("Starting GURT search server on {}", self.config.server_bind_address());
let scheduler_config = self.config.clone();
let _scheduler_handle = BackgroundScheduler::new(scheduler_config).start();
info!("Background crawler scheduler started");
let server = GurtServer::with_tls_certificates(
&self.config.server.cert_path.to_string_lossy(),
&self.config.server.key_path.to_string_lossy()
)?;
let search_engine = self.search_engine.clone();
let config = self.config.clone();
let server = server
.get("/search", {
let search_engine = search_engine.clone();
let config = config.clone();
move |ctx| {
let search_engine = search_engine.clone();
let config = config.clone();
let path = ctx.path().to_string();
async move {
handle_search(path, search_engine, config).await
}
}
})
.get("/api/search*", {
let search_engine = search_engine.clone();
let config = config.clone();
move |ctx| {
let search_engine = search_engine.clone();
let config = config.clone();
let path = ctx.path().to_string();
async move {
handle_api_search(path, search_engine, config).await
}
}
})
.get("/", {
move |_ctx| async {
Ok(GurtResponse::ok().with_string_body(include_str!("../frontend/search.html")))
}
})
.get("/search.lua", {
move |_ctx| async {
Ok(GurtResponse::ok().with_string_body(include_str!("../frontend/search.lua")))
}
})
.get("/health", |_ctx| async {
Ok(GurtResponse::ok().with_json_body(&json!({"status": "healthy"}))?)
})
.get("/test*", |ctx| {
let path = ctx.path().to_string();
async move {
println!("Test request path: '{}'", path);
Ok(GurtResponse::ok().with_string_body(format!("Path received: {}", path)))
}
});
info!("GURT search server listening on {}", self.config.gurt_protocol_url());
server.listen(&self.config.server_bind_address()).await.map_err(|e| anyhow::anyhow!("GURT server error: {}", e))
}
}
pub async fn run_server(config: Config) -> Result<()> {
let server = SearchServer::new(config).await?;
server.run().await
}
fn parse_query_param(path: &str, param: &str) -> String {
let param_with_eq = format!("{}=", param);
if let Some(start) = path.find(&format!("?{}", param_with_eq)) {
let start_pos = start + 1 + param_with_eq.len(); // Skip the '?' and 'param='
let query_part = &path[start_pos..];
let end_pos = query_part.find('&').unwrap_or(query_part.len());
urlencoding::decode(&query_part[..end_pos]).unwrap_or_default().to_string()
} else if let Some(start) = path.find(&format!("&{}", param_with_eq)) {
let start_pos = start + 1 + param_with_eq.len(); // Skip the '&' and 'param='
let query_part = &path[start_pos..];
let end_pos = query_part.find('&').unwrap_or(query_part.len());
urlencoding::decode(&query_part[..end_pos]).unwrap_or_default().to_string()
} else {
String::new()
}
}
fn parse_query_param_usize(path: &str, param: &str) -> Option<usize> {
let value = parse_query_param(path, param);
if value.is_empty() { None } else { value.parse().ok() }
}
async fn handle_search(
path: String,
search_engine: Arc<SearchEngine>,
config: Config
) -> Result<GurtResponse, GurtError> {
let query = parse_query_param(&path, "q");
if query.is_empty() {
return Ok(GurtResponse::bad_request()
.with_json_body(&json!({"error": "Query parameter 'q' is required"}))?);
}
println!("Search query: '{}'", query);
let limit = parse_query_param_usize(&path, "limit")
.unwrap_or(config.search.search_results_per_page)
.min(config.search.max_search_results);
match search_engine.search(&query, limit).await {
Ok(results) => {
let response = json!({
"query": query,
"results": results,
"count": results.len()
});
Ok(GurtResponse::ok()
.with_header("content-type", "application/json")
.with_json_body(&response)?)
}
Err(e) => {
error!("Search failed: {}", e);
Ok(GurtResponse::internal_server_error()
.with_json_body(&json!({"error": "Search failed", "details": e.to_string()}))?)
}
}
}
async fn handle_api_search(
path: String,
search_engine: Arc<SearchEngine>,
config: Config
) -> Result<GurtResponse, GurtError> {
let query = parse_query_param(&path, "q");
if query.is_empty() {
return Ok(GurtResponse::bad_request()
.with_json_body(&json!({"error": "Query parameter 'q' is required"}))?);
}
let page = parse_query_param_usize(&path, "page")
.unwrap_or(1)
.max(1);
let per_page = parse_query_param_usize(&path, "per_page")
.unwrap_or(config.search.search_results_per_page)
.min(config.search.max_search_results);
match search_engine.search_with_response(&query, page, per_page).await {
Ok(response) => {
Ok(GurtResponse::ok()
.with_header("content-type", "application/json")
.with_json_body(&response)?)
}
Err(e) => {
error!("API search failed: {}", e);
Ok(GurtResponse::internal_server_error()
.with_json_body(&json!({"error": "Search failed", "details": e.to_string()}))?)
}
}
}