search_hub

at 18c4440 Raw

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)
        }
    }
}