プログラミング
RustによるSearXNG
要約
SearXNGスタイルのメタ検索エンジン「SearXNG in Rust」は、Rustで開発されています。このプロジェクトは、複数の検索エンジンに同時にクエリを送信し、HTML結果をスクレイピングし、正規化されたURLで重複を除去し、Reciprocal Rank Fusion (RRF) を使用してランキングを行います。ライブラリとしてもサーバーとしても利用可能で、Rust 1.75以降が必要です。
全文翻訳
metadata-search-engine-rs Rustで書かれたSearXNGスタイルのメタ検索エンジン。
複数の検索エンジンにクエリを同時にファンアウトし、HTML結果をスクレイピングし、正規化されたURLで重複を除去し、Reciprocal Rank Fusion (RRF) を使用してランキングします。
仕組み
検索リクエストが GET /search?q=<query> に到着します。
クエリは、reqwest を介して DuckDuckGo、Brave、Startpage、Yahoo に同時に送信されます。
各エンジンは、scraper(Mozilla の html5ever 上の CSS セレクタ)を使用して HTML レスポンスを解析します。
結果は正規化された URL(追跡パラメータを削除、ロケールプレフィックスを削除、クエリパラメータをソート)で重複排除されます。
重複した URL はマージされ、RRF(スコア = Σ 1/(60 + rank) エンジン間)でスコアリングされます — 複数のエンジンによって返されたページはより高くランク付けされます。
トップの結果は JSON として返されます。
要件
Rust 1.75+
Cargo
インストール
ライブラリとして
cargo add metadata-search-engine-rs
または Cargo.toml に手動で追加します。
[dependencies]
metadata-search-engine-rs = "0.1"
サーバーとして(ソースから)
git clone https://github.com/MikeLuu99/searxng-rust
cd searxng-rust
cargo build --release
実行
PORT=8080 MAX_RESULTS=20 cargo run
デバッグログを有効にする:
RUST_LOG=debug cargo run
例
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 { client };
let results = engine.search("rust programming", 5).await?;
for r in results {
println!("{}\n {}", r.title, r.url);
}
Ok(())
}
すべてのエンジンにファンアウトして RRF スコアリングされた結果を取得する
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 { 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 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 { 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(())
}
API
GET /health
curl http://localhost:3000/health
{
"status": "ok"
}
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
ボディ
{"error": "query parameter 'q' is required"}
空のクエリの場合
ステータス 400
ボディ
{"error": "query parameter 'q' cannot be empty"}
すべてのエンジンが失敗した場合
ステータス 503
ボディ
{"error": "all engines failed to respond"}
テスト
すべての単体テスト
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
ライブテストはデフォルトではCIで実行されないように #[ignore] がマークされています。実際のサイトに対してHTMLセレクタがまだ機能するかどうかを確認するために、手動で実行してください。
ターミナルUI
ratatui ベースの TUI がクレートとして利用可能で、ここでインストールできます。コードは GitHub 経由でアクセスできます。
新しい検索エンジンの追加
src/engines/<name>.rs を作成します。
Arc<reqwest::Client> を保持する構造体を定義します。
SearchEngine トレイトを実装します。
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 で配線します。