use async_trait::async_trait; use serde::Deserialize; use crate::search_engines::{EngineError, ResultEntry, SearchEngine}; pub struct SearXng { pub instance: String, pub url_tpl: String, } impl SearXng { pub fn from_config(config: &toml::Table) -> Option<Box<dyn SearchEngine>> { let instance = config.get("instance")?.as_str()?.to_string(); let url_tpl = format!("{}/search?format=json&q={{}}", instance.trim_end_matches('/')); Some(Box::new(SearXng { instance, url_tpl })) } } #[derive(Deserialize)] struct SearXngResult { title: Option<String>, url: Option<String>, content: Option<String>, engine: Option<String>, } #[derive(Deserialize)] struct SearXngResponse { results: Vec<SearXngResult>, } #[async_trait] impl SearchEngine for SearXng { fn id(&self) -> &str { "searxng" } fn name(&self) -> &str { "SearXNG" } fn url_template(&self) -> &str { &self.url_tpl } fn selector(&self) -> &str { "" } async fn fetch_results( &self, query: &str, client: &reqwest::Client, ) -> Result<Vec<ResultEntry>, EngineError> { let url = self.search_url(query); let body = client .get(&url) .header("Accept", "application/json") .send() .await .map_err(|e| EngineError(format!("searxng fetch failed: {e}")))? .text() .await .map_err(|e| EngineError(format!("searxng read body failed: {e}")))?; let resp: SearXngResponse = serde_json::from_str(&body) .map_err(|e| EngineError(format!("searxng JSON parse failed: {e}")))?; let results: Vec<ResultEntry> = resp .results .into_iter() .filter_map(|r| { let title = r.title.unwrap_or_default(); let url = r.url?; if title.is_empty() { return None; } Some(ResultEntry { title, url, description: r.content, engine: r.engine.unwrap_or_else(|| "searxng".into()), }) }) .collect(); if results.is_empty() { Err(EngineError("no results found".into())) } else { Ok(results) } } }