🔍 SearXNG 自宅ホスト検索基盤
✍️ 執筆: Claude Opus 5
ラズパイで動く自前メタ検索。何が起きていて、今どうなっているのか
🔍 SearXNG とは何か(3行で)
- ユーザーが「エアコン 掃除」と入れる
- SearXNG が Bing・DuckDuckGo・Yandex などに同時に同じ質問を投げる
- 返ってきた結果を1つにまとめ、重複を消して返す
ただし、ここに落とし穴があります。
・SearXNG に制限が無くても、その先の Bing や Brave には制限があります。SearXNG は「無料の検索APIを生み出す装置」ではなく、上流の制限をまとめて受け止めるだけの窓口です。この誤解が、今回の一連の問題の根っこでした。
🧭 前提知識 — 検索を機械から使う3つの道
| 道 | 中身 | お金 | 止められるか |
|---|---|---|---|
| ① 検索API(有料) | 検索会社が「機械用の窓口」を正式に用意している。契約・認証キー・利用上限・サポートがある | 従量課金。Serperは2,500回で打ち止め、SerpApiは月250回の無料枠 | 契約なので原則止まらない。上限を超えたら課金か停止 |
| ② スクレイピング(無料) | 人間用の検索ページをプログラムで取ってきてHTMLを解析する。契約は無い | 0円 | 相手の一存でいつでも止まる。CAPTCHA・429・仕様変更 |
| ③ 自分で検索エンジンを作る | Webを自分でクロールして索引を作る | 莫大 | 止められないが、現実的でない |
なぜ検索APIは有料なのか
なぜ「複数のエンジンに聞く」(メタ検索)のか
- 1本が死んでも全滅しない … 今回まさにこれで助かりました。DuckDuckGoがCAPTCHAでも、Bingとyandexが結果を返す
- 結果の質が上がる … 検索エンジンごとに得意分野が違う。複数の結果を混ぜて重複を消すと、1本より広く拾える
- 1本あたりの負荷が下がる … 逆に言うと、エンジンを増やしすぎると相手への総リクエスト数が増える。これが今回の事故の伏線でした
🤝 行儀よく使う — 迷惑をかけないための線引き
まず事実 — robots.txt は何と言っているか(実測)
| エンジン | robots.txt の記述 |
|---|---|
| Bing | Disallow: /search |
| DuckDuckGo | Disallow: /html ・ Disallow: / |
| Yandex | Disallow: /search |
| Brave | Disallow: /search |
4社とも「検索結果ページを機械で取らないでほしい」と明示しています。
・つまり SearXNG のスクレイピングは、相手の意思表示に反しています。robots.txt に法的な強制力はありませんが、「知らなかった」では済まない性質のものです。ここを曖昧にしたまま「無料で無制限」と喜ぶのは、はっきり言って行儀が悪いです。
だからこう線を引く
| やること | 理由 |
|---|---|
| 個人利用の範囲を超えない | 1日数回〜十数回。商用サービスの裏側として不特定多数に提供しない |
| 止められたら素直に止まる | 429・CAPTCHAは相手の「やめて」という意思表示。逆らわない |
| 叩く回数を最小にする | 並列5→2に落とした。エンジンも3本だけ。同じ質問を繰り返さない |
| 失敗したエンジンは長く休ませる | 403/429で6〜24時間停止。3分ごとに突っつかない |
| 結果を再配布しない | 自分が読むために使う。検索結果そのものを公開・販売しない |
| 公式APIがあるならそちらへ移る | Brave は公式APIがある。お金を払うのが最もクリーンな道 |
やらないこと(これをやったら「行儀が悪い」を通り越す)
- CAPTCHAの自動突破 … 相手の防御を無効化する行為。検索したいだけならやる必要がない
- 住宅用プロキシでIPをぐるぐる変える … 検知回避が目的。出口端末の同意も不透明で、業者によっては他人の回線を勝手に借りている
- User-Agent を変えながら正体を隠す … 上と同じ。ローテーション自体が「隠す意図」の証拠になる
- ブロックされた直後に叩き直す … 相手の負荷を意図的に増やす
- bot を24時間回す … 個人利用の範囲を超える
本当にクリーンにしたいなら
🏠 いまラズパイで何が動いているか
192.168.1.9)の中身は、実はとてもシンプルです。📱 チャットアプリ (Vercel・東京)
│
│ https でトークン付きリクエスト
▼
🔒 Tailscale Funnel ← ドメイン不要で自宅に穴を開けずに公開
│
▼
🍓 ラズベリーパイ (自宅)
├─ nginx :8889 ← トークンを検証。合わなければ403
│ └─ GET /search 以外は404 ・ 3req/秒に制限
▼
└─ SearXNG :8888 ← Docker。localhost からしか触れない
│
│ ここから外(自宅の回線)へ出ていく
▼
🌐 Bing / DuckDuckGo web / Yandexそれぞれの役割
| 部品 | 役割 | なぜ必要か |
|---|---|---|
| SearXNG (Docker) | 検索の代理人 | 本体。8888番だが localhost 限定で、外からは直接触れない |
| nginx (8889) | 門番 | SearXNG 自身はパスワードの仕組みを持たない。合言葉(トークン)を確認する係を前に置いた |
| Tailscale Funnel | 自宅への安全な入口 | ルータに穴を開けず、独自ドメインも不要で外から届く。無料 |
| ニュースbot (cron) | 利用者その1 | 毎朝7時・夜19時に新聞記事を集める。localhost 直叩きなのでトークン不要 |
| チャットアプリ | 利用者その2 | ⚠️ 現在は使っていない(後述) |
📊 いまの稼働状況(2026-07-29時点・まずここを見る)
| 項目 | 現状 |
|---|---|
| 生きているエンジン | Bing / DuckDuckGo web / Yandex の3本 |
| 1検索あたりの結果数 | 25〜29件(直近6回連続テストで全件エラーなし) |
| 使っているのは誰か | ニュースbotのcronだけ(毎朝7時台・夜19時台) |
| チャットアプリ | ⚠️ 使っていない。7/25から Serper(有料API)のまま戻していない |
| 1日のリクエスト量 | bot実行が1日6〜8回程度。ごく低頻度 |
| 料金 | SearXNG・Tailscale ともに0円(電気代とラズパイ本体を除く) |
「SearXNGを直した」=「チャットの検索が変わった」ではありません。
・チャットは今も Serper を使っています。戻す判断はまだしていません(「テストで正しい結果が返ると確認できるまで戻さない」というユーザー方針)。
誰が何に責任を持つか(障害時はこの順に疑う)
| 層 | 自分で直せること | 代表的な症状 |
|---|---|---|
| ① チャットアプリ | クエリ数・タイムアウト・どの検索業者を使うか・フォールバック | SearXNGに到達しない / 撃ちすぎ |
| ② Tailscale Funnel・nginx | 外から届くか・合言葉の検証・入口のレート制限 | 403(合言葉違い)/ 404(許可外URL)/ 繋がらない |
| ③ Docker・SearXNG設定 | 起動・エンジンのON/OFF・休止時間・出力形式 | Restarting / JSONが壊れる / 特定エンジンだけ失敗 |
| ④ 上流の検索会社 | 何もできない(待つか、使うのをやめるか) | 429 / CAPTCHA / parsing error / 200なのに中身がゴミ |
| ⑤ 自宅回線・DNS | 回線の再起動くらい | 全エンジンが同時に死ぬ |
🔥 2026年7月に何が起きていたか(真因)
決定的だった実験
10回連続で叩く → 全部 HTTP 200・完全な検索結果 同時に4本叩く → 全部 200 同時に8本叩く → 全部 200 UAを付けずに叩く → 200 ↓ 累計で約35回叩いた後 ↓ 何をしても → HTTP 429(Too Many Requests)
この観測は「恒久的なIP BAN」ではなく「回復するレート制限」と整合します。
・数十回ぶん叩けて、使い切ると全部429になり、しばらくすると回復する。BANされ続けていたのではなく、回復するそばから使い切っていたと考えると全部つじつまが合います。 ※Brave内部の判定方式そのものを見たわけではありません。IP単位か、通信の指紋か、時間窓か、その組み合わせかまでは断定できません。あくまで「観測した挙動と最も整合する説明」です。
なぜ使い切っていたのか
ユーザーの検索 1回 = 5クエリ × エンジン4〜8個 = 上流へ 20〜40リクエスト
もうひとつの誤解 —「1週間解けない」も間違いだった
🪤 測定の罠 — ここで3回だまされた
- 1
罠 1
「件数が返る = 生きている」ではない
Google は10件返してきたので「復活した」と報告しかけました。中身を見たら8件が「Google」「ログイン - Google アカウント」「Google ドライブ」。同意画面のHTMLを結果としてパースしていただけでした。上流はブロック時にエラーを返さず、自分のログイン画面を HTTP 200 で返してきます。 - 2
罠 2
文字列 grep も当てにならない
ページに "captcha" や "challenge" が含まれるかで判定したら、正常なページのJavaScriptにも当たって誤検出しました。判定は「外部ドメインへのリンクが何個あるか」「検索結果らしい要素が何個あるか」で見るべきです。 - 3
罠 3
systemctl is-active | grep active は "inactive" にマッチする
「cloudflared と WARP が動いています」と報告しましたが、完全な誤報でした。"in-active" の中に "active" が入っているだけです。実際は3つとも未インストール。ユーザーに不要な不安を与えました。部分一致で状態を判定してはいけません。
🐍 どういうソフトなのか — APIではなくHTMLを読んでいる
braveapi など)が、既定で有効なものはほぼ全部スクレイピング版です。| 項目 | 実際 |
|---|---|
| 言語 | Python 3.14 |
| 形 | Webアプリ(Flask系)。granian というサーバで動く |
| 配布 | Docker イメージ searxng/searxng |
| エンジン実装 | 249個のPythonファイル(1エンジン=1ファイル) |
| 外部との繋ぎ方 | ❌ API ではなく ✅ HTMLスクレイピング(一部だけAPI版あり) |
| APIキー | 基本 不要。だから無料だが、だからブロックされる |
エンジン1本の中身(bing.py の実物)
request()(どう聞きに行くか)と response()(返ってきたHTMLをどう読むか)の2つの関数だけでできています。base_url = "https://www.bing.com"
def request(query, params):
# ← 人間が使うのと同じ検索ページのURLを組み立てているだけ
params["url"] = f"{base_url}/search?{urlencode(query_params)}"
def response(resp):
dom = html.fromstring(resp.text) # ← 返ってきたHTMLを解析
for item in eval_xpath_list(dom,
'//ol[@id="b_results"]/li[contains(@class, "b_algo")]'):
# ↑ Bingのページの「検索結果リスト」を XPath で名指しして掻き出す
link = eval_xpath_getindex(item, ".//h2/a", 0, None)
href = link.attrib.get("href", "")
title = extract_text(link)この作りが、これまでの症状を全部説明します。
・Startpage の「parsing error」 … 向こうがHTMLの構造を変えると、XPathが空振りして即死ぬ。SearXNG側の更新待ちになる
・Google が「ゴミ10件」を返す … ブロック時にエラーではなく同意画面のHTMLが返る。SearXNGは律儀にそれをパースして「Google」「ログイン」を10件の結果として返す
・CAPTCHA・429 … 相手からはブラウザではなくbotに見えている。当然の反応
・APIキーが要らない = 契約が無い … 契約が無いということは、止められても文句が言えない
braveapi.py / kagi.py / exaapi.py など)。スクレイピング版の brave と API版の braveapi は別エンジンです。API版はキーが必要な代わりに、IP評判やCAPTCHAへの依存を大きく減らせます(ただし完全に無関係になるわけではなく、キーごとの利用上限・契約条件・障害には依存します)。📁 プログラムの構成 — 言語・フレームワーク・フォルダ
技術スタック(実機で確認した実物)
| 層 | 使っているもの | 役割 |
|---|---|---|
| 言語 | Python 3.14.6 | 全部これ1つ。エンジン実装も設定読み込みも同じ |
| Webフレームワーク | Flask 3.1.3 | 「/search にGETが来たらこの関数を呼ぶ」という対応づけ。いわゆる薄いWebフレームワーク |
| アプリサーバ | Granian | Rust製。Flaskアプリを実際に動かして待ち受ける。起動は granian searx.webapp:app の1行 |
| HTTPクライアント | httpx 0.28.1 | 上流の検索エンジンへ出ていくときに使う。非同期(asyncio)で複数エンジンを同時に叩く |
| HTMLパーサ | lxml | 返ってきたHTMLをXPathで解析して結果を抜き出す |
| テンプレート | Jinja2 + flask_babel | HTML画面と多言語。うちは formats: [json] なので実質使っていない |
| 配布形態 | Docker イメージ searxng/searxng | ラズパイでは/usr/local/searxng/ 配下に展開されている |
httpx で取ってきて lxml で抜く」の249パターンです。フォルダ構成(行数は実測)
/usr/local/searxng/
entrypoint.sh 起動スクリプト。最後の1行が全て:
exec .venv/bin/granian searx.webapp:app
.venv/ Python仮想環境(Flask等はここに入っている)
searx/ ★本体。以下すべてこの下
webapp.py 1,413行 ★入口。FlaskのルーティングとHTTP処理
utils.py 813行 文字列処理・XPath補助などの共通関数
results.py 356行 複数エンジンの結果を1つにまとめ重複を消す
query.py 349行 クエリ解析。「!brave 検索語」のbangもここ
preferences.py 597行 ユーザー設定(言語・safesearch等)
settings_loader.py 設定ファイルの読み込みと既定値のマージ
cache.py 500行 結果キャッシュ
sqlitedb.py 477行 キャッシュ用の内蔵SQLite
locales.py 464行 言語判定
autocomplete.py 404行 検索候補
weather.py 669行 天気の整形(一部エンジン用)
engines/ 249本 32,462行 ★心臓部。1エンジン=1ファイル
search/ 9本 1,341行 ★検索の実行制御。並列に投げて集める
network/ 4本 1,038行 ★外への通信。httpxの設定・タイムアウト・エラー判定
result_types/ 8本 1,457行 結果の型(web/画像/論文/コード…)
botdetection/ 14本 1,410行 ★自分が受ける側の防御(limiter)。上流対策ではない
plugins/ 13本 1,293行 電卓・単位換算・トラッカー除去などの後処理
favicons/ 6本 952行 検索結果のファビコン取得
enginelib/ 3本 690行 エンジン共通の土台クラス
metrics/ 3本 662行 エンジン別の成功/失敗の統計
answerers/ 4本 360行 「1+1」等に直接答える機能
data/ 5本 402行 User-Agent一覧・言語データ等
templates/ static/ translations/ HTML画面用(うちでは未使用)各フォルダの解説(重要な5つ)
| フォルダ | 何をしている | ここが原因になる症状 |
|---|---|---|
| engines/ 249本・32,462行 | 1エンジン1ファイル。request() でURLを組み立て、response() でHTMLを解析する。全体の8割がここ | parsing error(相手のHTML変更)/特定エンジンだけ失敗 |
| search/ 9本・1,341行 | 「どのエンジンに投げるか」を決めて並列実行し、結果を集める司令塔。失敗したエンジンを一定時間サスペンドするのもここ | 一部エンジンが呼ばれない/サスペンドが効かない |
| network/ 4本・1,038行 | httpx の設定(タイムアウト・接続プール・プロキシ)。raise_for_httperror.py が 429/403/CAPTCHA を例外に変換する | タイムアウト/429・CAPTCHAの判定 |
| botdetection/ 14本・1,410行 | 自分が「攻撃される側」になったときの防御。IP制限やヘッダ検査。limiter: false で無効化している部分 | ⚠️ 上流にブロックされる問題とは無関係。ここをいじっても直らない |
| result_types/ ・ plugins/ | 結果の型付けと後処理(電卓・単位換算・トラッカー除去など) | 結果の見た目がおかしい程度。障害の主因にはなりにくい |
よくある勘違い: botdetection/ は上流対策ではありません
・botdetection/ と settings.yml の limiter は「SearXNGに入ってくるリクエストを制限する」機能です。
・今回のように「上流のBingやBraveからブロックされる」問題には一切効きません。むしろ関係するのは network/(出ていく設定)と search/(何本投げるか)です。
・名前が紛らわしいので、障害時にここを触らないよう注意してください。
自分で書くファイルはたった2つ
/home/satomata48/searxng/ settings.yml ★これだけ書く(現在92行)。既定346エンジンへの差分 settings.yml.bak6-… 変更ごとのバックアップ .auth_token nginxが確認する合言葉(chmod 600) /etc/nginx/conf.d/ searxng-proxy.conf ★門番。GET /search 以外は404・3req/秒
use_default_settings: true が効いています。コンテナ内の既定 settings.yml(346エンジン分の定義)を土台にして、自分のファイルは上書きしたい分だけを書きます。だから92行で足ります。⚙️ 検索1回がコードの中をどう流れるか
① webapp.py HTTPを受ける(Flask) ↓ ?q=... &format=json &categories=general を解釈 ② query.py クエリを解析。「!brave 〜」のbangをここで検出 ↓ → 使うエンジンのリストが決まる ③ search/__init__.py ★司令塔 ↓ Search._get_requests() │ 有効な全エンジンについて engines/xxx.py の │ request() を呼び、投げるURLを組み立てる ↓ Search.search_multiple_requests() │ ★ここで全エンジンへ「同時に」投げる ④ network/ httpx が実際に送信 ↓ ・request_timeout(既定3秒/うちは4秒) │ ・raise_for_httperror.py が 429/403/CAPTCHA を │ SearxEngineTooManyRequests 等の例外へ変換 ⑤ engines/xxx.py 各エンジンの response() が呼ばれる ↓ lxml でHTMLをパースし、XPathで結果を抜く │ 失敗すると例外 → そのエンジンは │ unresponsive_engines に記録され、一定時間サスペンド ⑥ results.py 全エンジンの結果を1つに統合 ↓ URLで重複排除・スコア計算・並び替え ⑦ webapp.py JSONにして返す
症状はどの段階で起きているか
| 症状 | 起きている段階 | 意味 |
|---|---|---|
too many requests | ④ network | 上流が429を返した。SearXNGは正常に検知している |
CAPTCHA | ④ network | 上流がbot判定した。同上 |
access denied | ④ network | 上流が403。同上 |
parsing error | ⑤ engines | 通信は成功している。相手のHTML構造が変わってXPathが空振りした |
| 200なのに結果がゴミ | ⑤ engines | 誰もエラーだと思っていない。同意画面のHTMLを律儀にパースした結果 |
| 結果が0件(エラーなし) | ②または③ | そのエンジンが呼ばれていない(disabled: true 等) |
| タイムアウト | ④ network | request_timeout を超えた。うちは4秒 |
一番厄介なのは「200なのに中身がゴミ」です
・④ネットワーク層は成功と判断し、⑤エンジン層もパースに成功し、⑥統合層も普通に10件として扱います。
・つまりシステムのどこにもエラーが記録されません。unresponsive_engines にも出ません。
・検出できるのは「中身を読む」ことだけです。件数やHTTPステータスでは絶対に分かりません。
「1検索が上流の何リクエストになるか」もここで決まる
search_multiple_requests() が、有効なエンジンの数だけ同時にリクエストを発射します。そしてアプリ側は1回の検索で複数クエリを投げます。この掛け算が今回の事故の正体でした。アプリの検索1回 × MAX_PARALLEL_QUERIES (旧5 → 現2) × 有効エンジン数 (旧8前後 → 現3) ──────────────────────────── = 上流への総リクエスト数 旧: 最大40 → 現: 6
💻 呼ぶ側(チャットアプリ)のコード構成
| 層 | 使っているもの |
|---|---|
| 言語 | TypeScript |
| フレームワーク | SvelteKit 2.9 + Svelte 5.2(画面もサーバも同じ枠組み) |
| ビルド | Vite 5.4 |
| ホスティング | Vercel(東京 hnd1)/Cloudflare Pages も並走 |
| DB | Turso(@libsql/client) |
| AI | Vercel AI SDK(ai / @openrouter/ai-sdk-provider) |
検索に関係するファイル
src/
routes/
api/chat/+server.ts ★どの検索業者を使うか決める場所
serperApiKey ? 'serper' : searxngReady ? 'searxng' : …
フォールバックの連鎖もここで組む
usage/+page.svelte 検索回数の可視化
lib/server/ ★サーバ側のロジック(26ファイル)
tools.ts ★検索ツール本体。SearXNG含む5プロバイダを実装
searchRelevance.ts 返ってきた結果が「クエリと無関係なゴミ」か判定
fetchWithTimeout.ts タイムアウト付きfetch(SearXNGは12秒)
concurrency.ts 並列実行の制御(mapWithConcurrency)
db/ ★DBを13ドメインに分割
usage.ts 検索回数の記録(searxng_count 等)
chats.ts research.ts misc.ts …
scripts/
probe-searxng-engines.mjs ★エンジンの生死を測る診断スクリプトtools.ts の中の SearXNG
export type SearchProvider =
'tavily' | 'exa' | 'serper' | 'google' | 'searxng';
// searxng の分岐(要点だけ)
const params = new URLSearchParams({
q: query,
format: 'json',
categories: 'general',
language: queryHasJapanese ? 'ja' : 'all'
});
const response = await fetchWithTimeout(
`${base}/search?${params}`,
{
headers: { 'X-Auth-Token': apiKey }, // nginxが確認する合言葉
redirect: 'manual' // 転送先へトークンを持ち越さない
},
12_000 // Funnelの往復が中央値6.4秒あるため
);安全側に倒している箇所(読むときの勘所)
| コード | なぜそうしているか |
|---|---|
su.protocol !== 'https:' で弾く | 設定ミスで合言葉を平文http や別ホストへ送らないための防波堤 |
redirect: 'manual' | 3xxで転送された先へ X-Auth-Token を持ち越さないため |
| content-type が JSON か確認 | Funnelやプロキシがエラー画面のHTMLを200で返した時に誤動作しないため |
MAX_PARALLEL_QUERIES = 2 | 上流への発射数を絞る(旧5)。今回の事故の直接対策 |
searchConcurrency = provider === 'searxng' ? 2 : … | SearXNG宛だけ同時実行を2に抑える |
searchRelevance.ts でゴミ判定 | 「200なのに中身がゴミ」を検出して有料APIへフォールバックするため |
settings.yml)は git 管理外で、アプリ(satomatashikiaichat)とはデプロイ経路も違います。「アプリを直したのにPiが古いまま」「Piを直したのにアプリが使っていない」が起きやすいので、変更時はどちら側の話か常に意識してください。🚦 いま生きているエンジン・死んでいるエンジン
!brave 検索語)があるので、1本ずつ切り分けられます。| エンジン | 状態 | 実測した中身 |
|---|---|---|
| Bing | ✅ 稼働中 | 常に10件前後を返す。ただし語順に弱く、たまに全く関係ない結果を返す |
| DuckDuckGo web | ✅ 稼働中 | 8/8成功。既定のDuckDuckGoとは別実装で、こちらは安定 |
| Yandex | ✅ 稼働中 | 日本語でも的確。今回新たに有効化した |
| DuckDuckGo (既定) | ❌ 停止させた | 約半数のクエリでCAPTCHA。しかも休まず毎回再挑戦する(理由は下の補足) |
| ❌ 停止させた | 同意/ログイン画面をHTTP 200で返す。SearXNGはそれを10件の「結果」として解釈してしまう | |
| Brave | ❌ 停止中 | 品質は一番良かったが、無料スクレイピングの予算がすぐ尽きる |
| Mojeek | ❌ 停止中 | 1回目のリクエストから access denied |
| Startpage / Qwant | ❌ 停止中 | パーサ破損 / CAPTCHA継続 |
これは自宅IPのせいなのか? → 違う
| エンジン | 公開インスタンス横断のエラー率 |
|---|---|
| Bing | 0% |
| Yandex | 0% |
| 10% | |
| DuckDuckGo | 60% |
| Brave | 95% |
| Mojeek / Qwant / Startpage | 100% |
※ただしこれは searx.space がある時点で公開インスタンス(多くはデータセンターIP)を計測した可用性の値です。うちの家庭回線での将来の安定を保証するものでも、検索品質を保証するものでもありません。定期的に測り直す前提の数字として扱ってください。
🛠 何を直したか・いまの状態
ラズパイ側(settings.yml)
| 変更 | 理由 |
|---|---|
| Yandex を有効化 | 実測で日本語も的確。世界横断でもエラー率0% |
| DuckDuckGo(既定)を停止 → DuckDuckGo web に切替 | 既定は約半数でCAPTCHA。しかも休止時間0がエンジン側にハードコードされており設定では直せない(下の補足) |
| Google / Google news を停止 | 同意画面をゴミ10件として返すので、あるだけ有害 |
| 失敗後の休止時間を延長 | 403/429を180秒→6〜24時間。3分ごとの再挑戦が評判を下げ続けていた |
| 同時接続を100→20に | 家庭用回線には過大だった |
①
settings.yml の suspended_times(失敗の種類ごとの共通設定)② エンジン自身が例外を投げるときに秒数を明示する(こちらが①より優先される)
DuckDuckGoエンジンは②で
suspended_time=0 を渡すよう実装されています(「DDGはIPをブロックしないから0でよい」という判断)。だから設定をいくら延ばしても休みません。設定では直せないので、エンジンごと止めるのが唯一の対処でした。ログにも
CAPTCHA (jp-jp) (suspended_time=0) と出ます。SearXNG の Issue #4824 で議論され、修正PR #5839 はクローズ済みです。アプリ側
結果(6回連続テスト)
エアコン+掃除 → 27件 bing, duckduckgo web, yandex エラーなし sqlite+index → 28件 bing, duckduckgo web, yandex エラーなし climate+policy → 25件 bing, duckduckgo web, yandex エラーなし 確定申告+やり方 → 29件 bing, duckduckgo web, yandex エラーなし 電気代+節約 → 26件 bing, duckduckgo web, yandex エラーなし raspberry+pi → 27件 bing, duckduckgo web, yandex エラーなし
| 検索語 | 変更前(Bing単独) | 変更後 |
|---|---|---|
| climate policy 2026 | YouTubeヘルプ・アラビア語のDLページ | 国連事務総長の気候演説・IEA State of Energy Policy 2026 |
| sqlite expression index | SQLite のトップページ | Indexes On Expressions(SQLite公式ドキュメント) |
| エアコン 掃除 自分で | 価格.com・通販ページ | メーカー監修の掃除手順 |
🧰 触り方(困ったときの手順)
ssh satomata48@192.168.1.9(鍵認証・同じLAN内)。設定ファイルは root 所有なので sudo が要ります。Dockerコンテナ名は searxng。⚠️ 診断そのものが上流の予算を食います。
・調べるたびに本物のリクエストが飛びます。実際、今回の調査で私が35回叩いた結果 Brave が429になりました。調査は1エンジンにつき数回まで・2〜3秒あけて。429やCAPTCHAを観測したらそのエンジンへのテストは即やめること(休止設定が効いている間に叩き直さない)。
① まず状態を見る
satomatashikiaichat で実行。エンジンごとの生死+「200でゴミを返していないか」まで判定する
node scripts/probe-searxng-engines.mjs
② ラズパイの中を直接見る
unresponsive_engines(応答しなかったエンジンと理由)と、各結果の engines(実際に返したエンジン)を見る
ssh satomata48@192.168.1.9 sudo docker logs searxng | tail -30 sudo docker exec searxng sh -c 'wget -qO- "http://127.0.0.1:8080/search?q=test&format=json&categories=general"'
③ エンジンを1本ずつ調べる
engines= パラメータは効きません。
・自宅nginxが許可パラメータを絞っているため素通りして「全エンジン実行」になります。1本に絞るときは必ずクエリ本文のbang(!brave 検索語)を使ってください。ここで2時間溶かしました。
④ 設定を変えるとき
- 設定は
/home/satomata48/searxng/settings.yml(root所有なのでsudo必須) - 必ずバックアップを取る(
settings.yml.bak6-20260729等が履歴として残っている) - YAMLの構造に注意。engines の項目は engines ブロックの中に、outgoing より前に置く。私はここを間違えて SearXNG を一度落としました(30秒で復旧)
- 変更後は
sudo docker restart searxng。起動失敗(Restarting表示)ならすぐバックアップから戻す
⑤ 変更のチェックリスト(この順にやれば戻せる)
起動失敗の原因は docker logs searxng の SearxSettingsException 行に出る
# 1. 必ずバックアップ(日付を入れる)
sudo cp /home/satomata48/searxng/settings.yml \
/home/satomata48/searxng/settings.yml.bak$(date +%Y%m%d)
# 2. 編集(sudo 必須)
sudo nano /home/satomata48/searxng/settings.yml
# 3. 再起動
sudo docker restart searxng && sleep 15
# 4. 起動したか確認(Restarting なら失敗)
sudo docker ps --filter name=searxng --format '{{.Status}}'
# 5. 失敗していたら即戻す
sudo cp /home/satomata48/searxng/settings.yml.bakYYYYMMDD \
/home/satomata48/searxng/settings.yml
sudo docker restart searxng- コンテナが
Up(Restartingでない) - ラズパイ内から叩いてJSONが返る
unresponsive_enginesが空か、想定内のエンジンだけ- 結果の
enginesに、生きているはずのエンジンが全部出ている - 検索語と関係のあるタイトルが返っている(件数だけで判断しない)
- 数クエリ連続で試して再現する(1回では分からない)
触ってはいけない場所
・コンテナの中の engines/*.py … SearXNG本体。更新で消えるし、壊すと原因不明の不具合になる
・コンテナの中の settings.yml … 既定値。上書きは自分の /home/satomata48/searxng/settings.yml 側でやる
・.auth_token の中身を画面に出す・貼る … 合言葉。過去に会話ログへ平文で出してローテーションした事故がある
⑥ CAPTCHAが出たとき(公式手順)
💰 有料にするなら — 価格の実額(2026-07 調査)
まず前提 — うちの実際の使用量
年月 Tavily Exa Serper SearXNG 合計 2026-07 565 60 285 188 1,098 ← ピーク 2026-06 313 71 0 0 384 2026-02 40 0 0 0 40 2026-01 199 0 0 0 199 2025-12 99 0 0 0 99 ──────────────────────────────────────────── 月平均 364件 / ピークでも 1,098件
価格表(月1,000〜3,000件の小規模での実効価格)
| サービス | 1,000件あたり | 無料枠 | 速度 | 注意 |
|---|---|---|---|---|
| OpenRouter Web Search Parallelエンジン | $1.00 | — | 速い | 前払い不要。10結果まで込み、追加は1件$0.001 |
| DataForSEO Standard | $0.60 🥇 | なし | ⚠️ 平均5分 公式の目標は45分 | 単価は最安。対話には使えない |
| Serper | $1.00 | 2,500回 | 速い・50qps | ⚠️ $50前払い・6か月で失効(後述) |
| DataForSEO Live | $2.00 | なし | 6秒 | |
| SearchApi | $4.00 | 初回100件 | 速い | 最低 $40/月 |
| You.com Search | $5.00 | 初回$100=約20,000件 | 速い | 本文まで込み・1回で最大100件。ただし毎月ではなく初回のみ |
| Brave Search API | $5.00 | $5/月=1,000件 | 速い・50qps | SearXNGに直接挿せる唯一の選択 |
| Perplexity Search API | $5.00 | — | 速い | 1リクエストに5クエリまとめられる=まとめれば実質$1 |
| Exa | $7.00 | 登録時$20+$10/月=約1,428件/月 | 速い | Deep Search は $12〜15/1,000 |
| Tavily | $8.00 | 1,000credit/月 | 速い | Advanced Searchは複数credit消費 |
| Kagi Search API | 未確認 | なし | 速い | 公開docsから価格が消えた。要ログイン |
| SerpApi | $25.00 | 250件/月 | 速い | 小規模では最も高い |
| Gemini Grounding | ($14/1,000) | 月5,000prompt無料 | 速い | 超過後はprompt単位ではなく実際のquery単位で課金 |
Serper の「$1/1,000」は、この規模では嘘になります
・Serperは最低 $50 の前払いで50,000クレジット。有効期限は6か月です。
・うちの使用量(月1,000件)だと6か月で6,000件しか使えず、44,000件分=$44を捨てることになります。
・実効単価は $1/1,000 ではなく約 $8.33/1,000。表示価格の8倍です。
・「単価が安い」と「自分にとって安い」は別物、という典型例です。
結論:無料枠だけで足ります
Brave Search API 1,000件/月 ($5クレジット自動付与) Tavily 1,000件/月 Exa 約1,428件/月 ($10/月クレジット) ────────────────────────────── 合計 約3,428件/月 すべて $0 うちのピーク月 = 1,098件 → 無料枠の1/3も使っていない
| 順位 | 選択肢 | 月額 | いつ使うか |
|---|---|---|---|
| 🥇 | 無料枠3社を正規に使い分ける | $0 | 常用。うちの量なら永久にこれで足りる |
| 🥈 | OpenRouter の Parallel | $1〜3 | 無料枠を超えた分。前払い不要で既存残高から引ける |
| 🥉 | You.com の初回$100(約20,000件) | $0 | 約18か月ぶんある。使い切りなので「保険」として持つ |
| 4位 | DataForSEO Standard | $0.6〜1.8 | ニュースbotなど5分待てる非同期用途のみ |
| ❌ | Serper を買い足す | $50 | この規模では非推奨。$44を捨てることになる |
| ❌ | SerpApi | $25/月〜 | 小規模では最も高い |
使えないもの(調べた結果)
| サービス | 状況 |
|---|---|
| Yahoo! JAPAN 検索API | ❌ 2013年に提供終了。現存するのはYOLP(地図・ローカル・気象)のみでウェブ検索APIは無い |
| Bing Search API | ❌ 2025年8月11日に廃止 |
| Google Custom Search JSON API | ❌ 新規受付終了・2027年1月1日で廃止 |
| 国内SEOツール系のAPI | ❌ 順位計測・登録キーワード専用がほとんど。汎用ウェブ検索を返すものは見つからず |
| Mojeek API | △ 存在するが価格非公開(問い合わせ制) |
| Marginalia API | △ 非商用は無料だが、独立系インデックスで日本語ニュース検索の代替にはならない |
| Kagi Search API | △ サービスは存在するが、公開ドキュメントから価格表記が消えた(要ログイン)。以前の$12/1,000は現行価格として確認できず |
| You.com | ⭕ 使える。ただし$100は初回のみで毎月ではない。使い切ると$5/1,000 |
📌 残っていること
① チャットアプリはまだ SearXNG を使っていない
現在、チャットの検索は Serper(有料API)が使われています。
・7/25にSearXNGから切り替えたまま、戻していません。ラズパイを使っているのはニュースbotのcron(朝7時・夜19時)だけです。 戻すには src/routes/api/chat/+server.ts の SERPER_DISABLED = true。ユーザー方針で「テストで正しい結果が返ると確認できるまで戻さない」としています。
② Brave 公式APIキー(推奨)
braveapi という専用エンジンが標準搭載されており、settings.yml にコメントで設定を用意済みです。③「無制限・無料」についての正直な結論
用語集
| 用語 | 意味 | このページでの出どころ |
|---|---|---|
| メタ検索 | 自分では検索せず、複数の検索エンジンに代理で聞いて結果をまとめる仕組み | SearXNG がこれ |
| スクレイピング | 人間向けWebページを機械で取得し、HTMLから必要な情報を抜き出すこと | SearXNGの基本動作 |
| XPath | HTMLの中の場所を指定する書き方。//ol[@id="b_results"] のような形 | bing.py が結果を抜くのに使う |
| robots.txt | サイト所有者が「ここは機械で取らないで」と表明するファイル | 4社とも検索結果を拒否している |
| レートリミット | 一定時間に何回まで、という上限。超えると 429 Too Many Requests | Braveで35回ほどで到達 |
| CAPTCHA | 「あなたはロボットですか」の確認。botと判定された合図 | DuckDuckGo・Qwantで発生 |
| IP評判 | そのIPアドレスが過去にどう振る舞ったかの評価。共有IPだと他人の行いも影響する | 今回は無実だった |
| ファンアウト | 1つの要求が下流で複数の要求に増えること | 1検索→5クエリ→4エンジン=20リクエスト |
| サスペンド | 失敗したエンジンをしばらく呼ばないようにする仕組み | suspended_times |
| カバーリング | (このページでは)予備の手段が本命の失敗を肩代わりすること | DDG既定が落ちてもDDG webが返す |
| Tailscale Funnel | ルータに穴を開けずに自宅のサービスを外へ公開する仕組み。ドメイン不要 | アプリ→ラズパイの入口 |
| CGNAT / MAP-E | 1つのIPv4を複数の契約者で共有する方式。他人の行いが自分に返る | 今回は該当しなかった |
関連ページ
- Cloudflare 10万リクエスト事件簿 — 同じ「ラズパイが叩きすぎた」系の事故。今回と根っこは同じ