
2026/08/04 1:41
Rustで実装されたSearXNG
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
metadata-search-engine-rs は、SearXNG に着想を得たオープンソースの Rust 製検索アグリゲーターであり、ユーザーがプライベートな検索エンジンをセルフホストすることを可能にします。複数のプロバイダー(DuckDuckGo、Brave、Startpage、Yahoo など)に対して同時クエリを実行することで単一情報源へのバイアスを排除し、結果を結合する際に見返しランク融合(Reciprocal Rank Fusion, RRF)という数学的ランキング手法 score = Σ 1/(60 + rank) を使用します。システムは追跡パラメータの削除、URL の正規化、エントリの重複排除などによりクリーンなデータを確保し、ヘルスチェック用エンドポイント(/health)と検索用エンドポイント(/search)を含むセキュアな JSON API を通じて結果を提示します。インストールは Cargo またはソースから行うことが容易で、ライブラリ統合とサーバー展開の両方をサポートしています(例:cargo run --release でポートと結果制限を設定可能)。ローカル利用のためには、ratatui によって支えられた専用ターミナルインターフェースがインタラクティブな体験を提供します。開発者はプロジェクトの SearchEngine トレイトを実装した新しいエンジンを追加することで機能拡張を容易に行い、タイムアウト管理も含まれています。このツールは組織に検索メタデータに対する完全な制御を可能にし、堅牢なエラーハンドリング(すべてのエンジン障害時などの 503 レスポンスを含む)と、自らのサーバーからのカスタマイズ可能なクエリ処理を搭載しています。本文
SearXNG スタイルのメタデータ検索エンジン(Rust)
Rust で記述された、SearXNG スタイルのメタデータ検索エンジンです。複数の検索エンジンにクエリを同時並列で送信し、HTML をスクレイピングして結果を返します。
仕組み
- リクエスト受付:
に検索リクエストが到達します。GET /search?q=<query> - 並列処理: クエリは
を使用して、DuckDuckGo、Brave、Startpage、Yahoo へのクエリを同時並列で送信します。reqwest - HTML パース: Mozilla の
を基盤とする CSS セレクトルの仕組み(html5ever
ライブラリ)を用いて HTML レスポンスを解析します。scraper - 重複排除: URL 正規化(パラメータ追跡、ロケールプレフィックス除去、クエリパラメータソート)により結果を重複排除します。
- スコアリング:
- 重複した URL は統合され、**相互順位フュージョン(Reciprocal Rank Fusion, RRF)**でスコアリングされます。
- スコア計算式:
score = Σ 1/(60 + rank) - 複数のエンジンから返されたページほど、高いスコアを獲得します。
- レスポンス: 上位の結果が JSON 形式で返却されます。
要件
- Rust 1.75 以降
- Cargo
インストール
ライブラリとして利用する場合
Cargo.toml に以下の記述を追加します:
[dependencies] metadata-search-engine-rs = "0.1"
または、
cargo add を使用することも可能です:
cargo add metadata-search-engine-rs
サーバーとして利用する場合(ソースコードから)
git clone https://github.com/MikeLuu99/searxng-rust cd metadata-search-engine-rs cargo build --release
実行方法
環境変数を指定して実行します:
PORT=8080 MAX_RESULTS=20 cargo run --release
デバッグログを有効にするには、適切な環境変数または設定ファイルを指定してください。
使用例
単一のエンジンを実行する場合
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 client = Arc::new(build_http_client()?); let engine = DuckDuckGoEngine::new(client); let results = engine.search("rust programming", 5).await?; for r in results { println!("{}\n {}", r.title, r.url); } Ok(()) }
すべてのエンジンにクエリを広げ、RRF ランキングを取得する場合
複数のエンジン(DuckDuckGo、Brave、Startpage、Yahoo)を使用し、スコア付きの結果を統合します。
コード例:
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 client = Arc::new(build_http_client()?); let engines: Vec<Arc<dyn SearchEngine>> = vec![ Arc::new(DuckDuckGoEngine::new(Arc::clone(&client))), Arc::new(BraveEngine::new(Arc::clone(&client))), Arc::new(StartpageEngine::new(Arc::clone(&client))), Arc::new(YahooEngine::new(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(()) }
特定のエンジンのみを使用する場合
指定したエンジン(例:DuckDuckGo と Brave)のみを使用し、スニペットを含む結果を出力します。
コード例:
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 client = Arc::new(build_http_client()?); let engines: Vec<Arc<dyn SearchEngine>> = vec![ Arc::new(DuckDuckGoEngine::new(Arc::clone(&client))), Arc::new(BraveEngine::new(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(()) }
API
: サーバーの状態確認GET /health
例:curl http://localhost:3000/health
: 検索実行GET /search?q=<query>
例:curl "http://localhost:3000/search?q=rust"
レスポンス例:
{ "query": "rust", "results": [ { "title": "Rust Programming Language", "url": "https://rust-lang.org/", "snippet": "A language empowering everyone to build reliable and efficient software.", "engines": ["duckduckgo", "brave", "startpage", "yahoo"], "score": 0.049 } ], "engines_queried": ["duckduckgo", "brave", "startpage", "yahoo"], "engines_failed": [] }
エラーレスポンス
| 状況 | ステータスコード | ボディ |
|---|---|---|
パラメータの欠落 | 400 | |
が空である | 400 | |
| すべてのエンジンでエラーが発生した場合 | 503 | |
テスト
全単位テスト
cargo test
特定のモジュールのみをテストする場合
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
リアル環境でのテスト(インターネット接続が必要)
cargo test -- --ignored test_live
※注意: リアル環境テストは
#[ignore] でマークされているため、デフォルトでは CI には実行されません。手動で実行して、HTML セレクトが実際のサイト上で正しく動作するか確認してください。
ターミナルユーザーインターフェース(TUI)
ratatui ベースの TUI クレイトも利用可能です。GitHub からコードにアクセスできます。
新しい検索エンジンの追加方法
-
を作成します。src/engines/<name>.rs -
を保持する構造体を実装します。Arc<reqwest::Client> -
トレイトを実装します:SearchEngineimpl 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 { // HTML を取得し、scraper で解析して Vec<SearchResult> を返す }) } } -
に追加し、engines/mod.rs
で連携します。main.rs -
構造体に
フィールドと、既存のエンジンと同様のtimeout: Duration
コンストラクタペアを追加し、タイムアウトをモジュール内の検索関数に渡すことを忘れないでください。new()/with_timeout()