add search engine - ringle
This commit is contained in:
195
search-engine/src/server.rs
Normal file
195
search-engine/src/server.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
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()}))?)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user