SearXNG in Rust

Aug 03, 2026 11:41 PM - 2 hours ago 1

A SearXNG-style metadata hunt motor written successful Rust. Fans retired queries to aggregate hunt engines concurrently, scrapes their HTML results, deduplicates by normalized URL, and ranks utilizing Reciprocal Rank Fusion (RRF).

  1. A hunt petition arrives astatine GET /search?q=<query>
  2. The query is sent concurrently to DuckDuckGo, Brave, Startpage, and Yahoo via reqwest
  3. Each motor parses the HTML consequence pinch scraper (CSS selectors complete Mozilla's html5ever)
  4. Results are deduplicated by normalized URL (tracking params stripped, locale prefixes removed, query params sorted)
  5. Duplicate URLs are merged and scored pinch RRF (score = Σ 1/(60 + rank) crossed engines) — pages returned by aggregate engines rank higher
  6. The apical results are returned arsenic JSON
  • Rust 1.75+
  • Cargo
cargo adhd metadata-search-engine-rs

Or adhd manually to Cargo.toml:

[dependencies] metadata-search-engine-rs = "0.1"

As a server (from source)

git clone https://github.com/MikeLuu99/searxng-rust cd metadata-search-engine-rs cargo build --release
PORT=8080 MAX_RESULTS=20 cargo tally --release

Enable debug logging:

Add to your Cargo.toml:

[dependencies] metadata-search-engine-rs = "0.1" tokio = { version = "1", features = ["full"] }
use std::sync::Arc; use metadata_search_engine_rs::engines::{DuckDuckGoEngine, SearchEngine, build_http_client}; #[tokio::main] async fn main() -> anyhow::Result<()> { let customer = Arc::new(build_http_client()?); let motor = DuckDuckGoEngine { customer }; let results = engine.search("rust programming", 5).await?; for r in results { println!("{}\n {}", r.title, r.url); } Ok(()) }

Fan retired to each engines and get RRF-ranked results

use std::sync::Arc; use metadata_search_engine_rs::{ aggregator::{aggregate, query_all_engines}, engines::{BraveEngine, DuckDuckGoEngine, SearchEngine, StartpageEngine, YahooEngine, build_http_client}, }; #[tokio::main] async fn main() -> anyhow::Result<()> { let customer = Arc::new(build_http_client()?); let engines: Vec<Arc<dyn SearchEngine>> = vec![ Arc::new(DuckDuckGoEngine { client: Arc::clone(&client) }), Arc::new(BraveEngine { client: Arc::clone(&client) }), Arc::new(StartpageEngine { client: Arc::clone(&client) }), Arc::new(YahooEngine { client: Arc::clone(&client) }), ]; let (successes, failures) = query_all_engines(&engines, "rust programming", 10).await; for (name, err) in &failures { eprintln!("engine {name} failed: {err}"); } let results = aggregate(successes, 10); for r in &results { println!("[{:.3}] ({}) {}", r.score, r.engines.join(", "), r.title); println!(" {}", r.url); } Ok(()) }

Use only circumstantial engines

use std::sync::Arc; use metadata_search_engine_rs::{ aggregator::{aggregate, query_all_engines}, engines::{BraveEngine, DuckDuckGoEngine, SearchEngine, build_http_client}, }; #[tokio::main] async fn main() -> anyhow::Result<()> { let customer = Arc::new(build_http_client()?); let engines: Vec<Arc<dyn SearchEngine>> = vec![ Arc::new(DuckDuckGoEngine { client: Arc::clone(&client) }), Arc::new(BraveEngine { client: Arc::clone(&client) }), ]; let (successes, _) = query_all_engines(&engines, "tokio async rust", 5).await; for r in aggregate(successes, 5) { println!("{} — {}", r.title, r.url); if let Some(snippet) = r.snippet { println!(" {snippet}"); } } Ok(()) }
curl http://localhost:3000/health
curl "http://localhost:3000/search?q=rust"
{ "query": "rust", "results": [ { "title": "Rust Programming Language", "url": "https://rust-lang.org/", "snippet": "A connection empowering everyone to build reliable and businesslike software.", "engines": ["duckduckgo", "brave", "startpage", "yahoo"], "score": 0.049 } ], "engines_queried": ["duckduckgo", "brave", "startpage", "yahoo"], "engines_failed": [] }

Error responses:

Case Status Body
Missing q 400 {"error": "query parameter 'q' is required"}
Empty q 400 {"error": "query parameter 'q' cannot beryllium empty"}
All engines fail 503 {"error": "all engines grounded to respond"}
# All portion tests cargo test # Specific module cargo test normalizer cargo test aggregator cargo test engines::duckduckgo cargo test engines::brave cargo test engines::startpage cargo test engines::yahoo cargo test server::handlers # Live tests (hit existent hunt engines — requires internet) cargo test -- --ignored test_live

Live tests are marked #[ignore] truthful they don't tally successful CI by default. Run them manually to verify HTML selectors still activity against the existent sites.

A ratatui-based TUI is disposable arsenic a crate to instal here. Access the codification via github

stx TUI

Adding a caller hunt engine

  1. Create src/engines/<name>.rs
  2. Define a struct holding Arc<reqwest::Client>
  3. Implement the SearchEngine trait:
impl SearchEngine for MyEngine { fn name(&self) -> &'static str { "myengine" } fn search<'a>( &'a self, query: &'a str, max_results: usize, ) -> BoxFuture<'a, Result<Vec<SearchResult>, EngineError>> { Box::pin(async move { // fetch HTML, parse pinch scraper, return Vec<SearchResult> }) } }

Add it to engines/mod.rs and ligament it successful main.rs

More