さとまたwiki

🔍 SearXNG 自宅ホスト検索基盤

✍️ 執筆: Claude Opus 5

ラズパイで動く自前メタ検索。何が起きていて、今どうなっているのか

2026-07-29 更新 ・ 2026-07-23 構築

🔍 SearXNG とは何か(3行で)

SearXNG は「検索エンジンそのもの」ではなく、複数の検索エンジンに代理で聞きに行く仲介役(メタ検索エンジン)です。自分では検索結果を1件も持っていません。
  • ユーザーが「エアコン 掃除」と入れる
  • SearXNG が Bing・DuckDuckGo・Yandex などに同時に同じ質問を投げる
  • 返ってきた結果を1つにまとめ、重複を消して返す
導入した理由はお金です。検索APIは有料で、Serper は2,500回で打ち止め、SerpApi は月250回。「回数を気にせず検索したい」ので、自宅のラズパイに自前で立てました。SearXNG 自体には回数制限がありません。

ただし、ここに落とし穴があります。

・SearXNG に制限が無くても、その先の Bing や Brave には制限があります。SearXNG は「無料の検索APIを生み出す装置」ではなく、上流の制限をまとめて受け止めるだけの窓口です。この誤解が、今回の一連の問題の根っこでした。

🧭 前提知識 — 検索を機械から使う3つの道

「プログラムから検索したい」と思ったとき、道は大きく3つしかありません。ここを理解すると、以降の話が全部つながります。
中身お金止められるか
① 検索API(有料)検索会社が「機械用の窓口」を正式に用意している。契約・認証キー・利用上限・サポートがある従量課金。Serperは2,500回で打ち止め、SerpApiは月250回の無料枠契約なので原則止まらない。上限を超えたら課金か停止
② スクレイピング(無料)人間用の検索ページをプログラムで取ってきてHTMLを解析する。契約は無い0円相手の一存でいつでも止まる。CAPTCHA・429・仕様変更
③ 自分で検索エンジンを作るWebを自分でクロールして索引を作る莫大止められないが、現実的でない
SearXNG は②の自動化ツールです。①の代わりにはなりません。ここを「無料の検索APIが手に入る」と誤解すると、必ず今回のような事故になります。

なぜ検索APIは有料なのか

検索会社は、世界中のWebを巡回して集め、索引を作り、常に更新し、1件ごとに計算資源を使って答えを返しています。1回の検索には実費がかかります。人間向けの検索が無料なのは広告を見てもらえるからで、機械が広告を見ずに結果だけ持っていくのは、向こうにとって「コストだけ払う客」です。
だからスクレイピングは嫌われます。相手の負担で自分だけ得をする形になるからです。CAPTCHAや429(Too Many Requests)は嫌がらせではなく、コストを守るための正当な防御です。「ブロックされた」と怒るのは筋違いで、こちらが行儀よく使う(=頻度を落とす)しかありません

なぜ「複数のエンジンに聞く」(メタ検索)のか

  • 1本が死んでも全滅しない … 今回まさにこれで助かりました。DuckDuckGoがCAPTCHAでも、Bingとyandexが結果を返す
  • 結果の質が上がる … 検索エンジンごとに得意分野が違う。複数の結果を混ぜて重複を消すと、1本より広く拾える
  • 1本あたりの負荷が下がる … 逆に言うと、エンジンを増やしすぎると相手への総リクエスト数が増える。これが今回の事故の伏線でした

🤝 行儀よく使う — 迷惑をかけないための線引き

この基盤は「相手のコストで動いている」ものです。迷惑をかけずに使いたいという方針のもとで、事実と線引きを正直に書きます。

まず事実 — robots.txt は何と言っているか(実測)

エンジンrobots.txt の記述
BingDisallow: /search
DuckDuckGoDisallow: /htmlDisallow: /
YandexDisallow: /search
BraveDisallow: /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時間回す … 個人利用の範囲を超える
今回の対処は、全部この方針と同じ向きでした。「並列を減らす」「エンジンを絞る」「休止時間を延ばす」「ゴミを返すGoogleを止める」——これらは相手への負荷を下げる方向の変更で、たまたま自分たちの安定にも効いた、という順番です。逆に「IPを変えて逃げる」「プロキシを噛ませる」は検討して採らないと決めました。

本当にクリーンにしたいなら

Brave Search API を契約するのが正解です。独自のインデックスを持ち、機械利用のための正式な窓口を有料で提供しています(目安 $3〜5 / 1,000検索・無料枠あり)。お金を払えば、相手も歓迎してくれます。「無料でやる」ことに固執すると、必ずどこかで誰かの善意にただ乗りすることになります。
※なお Bing の公式検索APIは2025年8月に廃止され、DuckDuckGo・Yandex・Mojeek には SearXNG から使える公式API版エンジンがありません。つまりお金で解決できるのは現状 Brave だけです。

🏠 いまラズパイで何が動いているか

自宅のラズパイ(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⚠️ 現在は使っていない(後述)
Tailscale は「入口」専用です。検索エンジンへ出ていく通信は Tailscale を通らず、ふつうに自宅の回線から出ます。ここを混同すると原因究明がぐちゃぐちゃになります(実際、私も一度そこを疑って確認しました。exit node は未使用で、出口は自宅IPのままでした)。

📊 いまの稼働状況(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回線の再起動くらい全エンジンが同時に死ぬ
④が原因のときは「直す」ことはできません。できるのは「叩く回数を減らす」「そのエンジンを止める」「公式APIに切り替える」の3つだけです。ここを勘違いして設定をいじり倒すと、今回のように自分で症状を悪化させます

🔥 2026年7月に何が起きていたか(真因)

7月下旬、検索結果が急にゴミになり、「1週間以上ブロックが解けない」という状態が続きました。IPを変えるかISPを変えるしかないのでは、というところまで疑いました。結論から言うとどちらも不要でした

決定的だった実験

自宅のラズパイから、ブラウザのふりをして Brave を直接叩きました。
10回連続で叩く   → 全部 HTTP 200・完全な検索結果
同時に4本叩く    → 全部 200
同時に8本叩く    → 全部 200
UAを付けずに叩く → 200

   ↓ 累計で約35回叩いた後 ↓

何をしても       → HTTP 429(Too Many Requests)

この観測は「恒久的なIP BAN」ではなく「回復するレート制限」と整合します。

・数十回ぶん叩けて、使い切ると全部429になり、しばらくすると回復する。BANされ続けていたのではなく、回復するそばから使い切っていたと考えると全部つじつまが合います。 ※Brave内部の判定方式そのものを見たわけではありません。IP単位か、通信の指紋か、時間窓か、その組み合わせかまでは断定できません。あくまで「観測した挙動と最も整合する説明」です。

なぜ使い切っていたのか

チャットアプリの検索が 1回で最大5クエリを同時に投げる設計でした。そして SearXNG は1クエリを複数のエンジンへ配ります。
ユーザーの検索 1回
  = 5クエリ × エンジン4〜8個
  = 上流へ 20〜40リクエスト
つまり検索1〜2回で予算が消滅していました。そして SearXNG は失敗したエンジンを180秒後に自動で再挑戦します。予算が尽きた状態で3分ごとに叩き続ければ、当然ずっと429です。「永久にブロックされている」ように見えていた正体はこれでした。
参考: SearXNG の公式Issue #2515 でも「Googleは新しいSearXNGインスタンスを5回連続検索しただけでブロックする」と報告されています。上流の許容量は、思っているよりずっと小さいです。

もうひとつの誤解 —「1週間解けない」も間違いだった

7/25に brave・mojeek・startpage を停止させたので、その後の4日間、誰もそれらに問い合わせていませんでした。つまり「まだ塞がっている」という観測自体が存在しなかったのです。実際、今回はじめて問い合わせたら Brave はいきなり20件の良質な結果を返しました。

🪤 測定の罠 — ここで3回だまされた

この調査で、私(Claude)は3回まちがった結論を出しました。同じ穴に落ちないための記録です。
  1. 1

    罠 1

    「件数が返る = 生きている」ではない

    Google は10件返してきたので「復活した」と報告しかけました。中身を見たら8件が「Google」「ログイン - Google アカウント」「Google ドライブ」。同意画面のHTMLを結果としてパースしていただけでした。上流はブロック時にエラーを返さず、自分のログイン画面を HTTP 200 で返してきます。
  2. 2

    罠 2

    文字列 grep も当てにならない

    ページに "captcha" や "challenge" が含まれるかで判定したら、正常なページのJavaScriptにも当たって誤検出しました。判定は「外部ドメインへのリンクが何個あるか」「検索結果らしい要素が何個あるか」で見るべきです。
  3. 3

    罠 3

    systemctl is-active | grep active は "inactive" にマッチする

    「cloudflared と WARP が動いています」と報告しましたが、完全な誤報でした。"in-active" の中に "active" が入っているだけです。実際は3つとも未インストール。ユーザーに不要な不安を与えました。部分一致で状態を判定してはいけません。
共通する教訓: 外部サービスの生死は「返ってきたか」ではなく「返ってきた中身が期待どおりか」で判定する。HTTPステータスも文字列一致も、ブロックの検出には使えません。

🐍 どういうソフトなのか — APIではなくHTMLを読んでいる

ここが一番の誤解ポイントです。主要なWeb検索エンジンに対しては、SearXNG は検索APIを使っていません。各検索サイトの「人間が見るページ」を取ってきて、HTMLから結果を掻き出しています(スクレイピング)。一部だけ公式API用のエンジンも用意されています(後述の 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キーが要らない = 契約が無い … 契約が無いということは、止められても文句が言えない

例外的にAPI版もあります。249本のうち一部は公式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フレームワーク
アプリサーバGranianRust製。Flaskアプリを実際に動かして待ち受ける。起動は granian searx.webapp:app の1行
HTTPクライアントhttpx 0.28.1上流の検索エンジンへ出ていくときに使う。非同期(asyncio)で複数エンジンを同時に叩く
HTMLパーサlxml返ってきたHTMLをXPathで解析して結果を抜き出す
テンプレートJinja2 + flask_babelHTML画面と多言語。うちは formats: [json] なので実質使っていない
配布形態Docker イメージ searxng/searxngラズパイでは/usr/local/searxng/ 配下に展開されている
「Flask + httpx + lxml」の3つが分かれば読めます。Flask が入口、httpx が外への足、lxml が読み取り。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 等)
タイムアウト④ networkrequest_timeout を超えた。うちは4秒

一番厄介なのは「200なのに中身がゴミ」です

・④ネットワーク層は成功と判断し、⑤エンジン層もパースに成功し、⑥統合層も普通に10件として扱います。

・つまりシステムのどこにもエラーが記録されません。unresponsive_engines にも出ません。

・検出できるのは「中身を読む」ことだけです。件数やHTTPステータスでは絶対に分かりません。

「1検索が上流の何リクエストになるか」もここで決まる

③の search_multiple_requests() が、有効なエンジンの数だけ同時にリクエストを発射します。そしてアプリ側は1回の検索で複数クエリを投げます。この掛け算が今回の事故の正体でした。
アプリの検索1回
  × MAX_PARALLEL_QUERIES        (旧5 → 現2)
  × 有効エンジン数               (旧8前後 → 現3)
  ────────────────────────────
  = 上流への総リクエスト数        旧: 最大40 → 現: 6
ここが「設定を1行変えるだけで効く」場所です。エンジンを1本増やすと、全部の検索でその分リクエストが増えます。「とりあえず全部有効に」が最も危険な操作だと分かります。

💻 呼ぶ側(チャットアプリ)のコード構成

SearXNG を使う側、satomatashikiaichat のコードです。こちらは Python ではなく TypeScript です。
使っているもの
言語TypeScript
フレームワークSvelteKit 2.9 + Svelte 5.2(画面もサーバも同じ枠組み)
ビルドVite 5.4
ホスティングVercel(東京 hnd1)/Cloudflare Pages も並走
DBTurso(@libsql/client
AIVercel 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

検索プロバイダは型で5つに固定されています。SearXNG専用のSDKなどは無く、ただのHTTP GET 1本です。
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へフォールバックするため
2つのリポジトリは別物です。ラズパイの設定(settings.yml)は git 管理外で、アプリ(satomatashikiaichat)とはデプロイ経路も違います。「アプリを直したのにPiが古いまま」「Piを直したのにアプリが使っていない」が起きやすいので、変更時はどちら側の話か常に意識してください。

🚦 いま生きているエンジン・死んでいるエンジン

2026-07-29 に全エンジンを1つずつ実測しました。SearXNG は各エンジンを名指しできる「bang」という記法(!brave 検索語)があるので、1本ずつ切り分けられます。
エンジン状態実測した中身
Bing✅ 稼働中常に10件前後を返す。ただし語順に弱く、たまに全く関係ない結果を返す
DuckDuckGo web✅ 稼働中8/8成功。既定のDuckDuckGoとは別実装で、こちらは安定
Yandex✅ 稼働中日本語でも的確。今回新たに有効化した
DuckDuckGo (既定)❌ 停止させた約半数のクエリでCAPTCHA。しかも休まず毎回再挑戦する(理由は下の補足)
Google❌ 停止させた同意/ログイン画面をHTTP 200で返す。SearXNGはそれを10件の「結果」として解釈してしまう
Brave❌ 停止中品質は一番良かったが、無料スクレイピングの予算がすぐ尽きる
Mojeek❌ 停止中1回目のリクエストから access denied
Startpage / Qwant❌ 停止中パーサ破損 / CAPTCHA継続

これは自宅IPのせいなのか? → 違う

SearXNG の公開インスタンス(世界中のサーバ)を横断して計測している searx.space のデータでは、エラー率はこうなっています。
エンジン公開インスタンス横断のエラー率
Bing0%
Yandex0%
Google10%
DuckDuckGo60%
Brave95%
Mojeek / Qwant / Startpage100%
Brave スクレイピングは世界中で死んでいます。うちの回線やIPが悪いわけではありませんでした。逆に Bing と Yandex は 0%「ブロックされないエンジンだけを使う」方針には十分な根拠があります。
※ただしこれは searx.space がある時点で公開インスタンス(多くはデータセンターIP)を計測した可用性の値です。うちの家庭回線での将来の安定を保証するものでも、検索品質を保証するものでもありません。定期的に測り直す前提の数字として扱ってください。

🛠 何を直したか・いまの状態

ラズパイ側(settings.yml)

変更理由
Yandex を有効化実測で日本語も的確。世界横断でもエラー率0%
DuckDuckGo(既定)を停止 → DuckDuckGo web に切替既定は約半数でCAPTCHA。しかも休止時間0がエンジン側にハードコードされており設定では直せない(下の補足)
Google / Google news を停止同意画面をゴミ10件として返すので、あるだけ有害
失敗後の休止時間を延長403/429を180秒→6〜24時間。3分ごとの再挑戦が評判を下げ続けていた
同時接続を100→20に家庭用回線には過大だった
「休止時間を延ばしたのに DuckDuckGo だけ効かない」の理由。休止時間には2つの決まり方があります。
settings.ymlsuspended_times(失敗の種類ごとの共通設定)
エンジン自身が例外を投げるときに秒数を明示する(こちらが①より優先される)
DuckDuckGoエンジンは②で suspended_time=0 を渡すよう実装されています(「DDGはIPをブロックしないから0でよい」という判断)。だから設定をいくら延ばしても休みません。設定では直せないので、エンジンごと止めるのが唯一の対処でした。
ログにも CAPTCHA (jp-jp) (suspended_time=0) と出ます。SearXNG の Issue #4824 で議論され、修正PR #5839 はクローズ済みです。

アプリ側

同時に投げるクエリ数を 5 → 2 に下げました。速度のために往復を並列化するのは正しいのですが、上流への発射数はそれとは別に絞る必要がある、というのが今回の学びです。

結果(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 2026YouTubeヘルプ・アラビア語のDLページ国連事務総長の気候演説・IEA State of Energy Policy 2026
sqlite expression indexSQLite のトップページIndexes On Expressions(SQLite公式ドキュメント)
エアコン 掃除 自分で価格.com・通販ページメーカー監修の掃除手順

🧰 触り方(困ったときの手順)

前提: 操作は自分のPC(リポジトリのあるマシン)から。ラズパイへは 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
合格の条件(全部満たすまで「直った」と言わない)
  • コンテナが UpRestarting でない)
  • ラズパイ内から叩いてJSONが返る
  • unresponsive_engines が空か、想定内のエンジンだけ
  • 結果の engines に、生きているはずのエンジンが全部出ている
  • 検索語と関係のあるタイトルが返っている(件数だけで判断しない)
  • 数クエリ連続で試して再現する(1回では分からない)

触ってはいけない場所

・コンテナの中の engines/*.py … SearXNG本体。更新で消えるし、壊すと原因不明の不具合になる

・コンテナの中の settings.yml … 既定値。上書きは自分の /home/satomata48/searxng/settings.yml 側でやる

・.auth_token の中身を画面に出す・貼る … 合言葉。過去に会話ログへ平文で出してローテーションした事故がある

⑥ CAPTCHAが出たとき(公式手順)

SearXNG公式には「サーバの出口IPからブラウザでCAPTCHAを解けば、そのIPがしばらく許可される」という手順があります。通常はSSHのSOCKSトンネル経由でやりますが、うちはラズパイもPCも同じ自宅回線なので、普通にブラウザでそのサイトを開いてCAPTCHAを解くだけで済みます

💰 有料にするなら — 価格の実額(2026-07 調査)

「無料にこだわらず、お金を払って安定させる」場合の選択肢。各社の公式ページで確認した実額です(Claude と Codex の二重確認)。

まず前提 — うちの実際の使用量

年月       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件」規模なので、大口向けの安い単価はどれも適用されません。逆に各社の無料枠で足りてしまいます

価格表(月1,000〜3,000件の小規模での実効価格)

サービス1,000件あたり無料枠速度注意
OpenRouter Web Search
Parallelエンジン
$1.00速い前払い不要。10結果まで込み、追加は1件$0.001
DataForSEO Standard$0.60 🥇なし⚠️ 平均5分
公式の目標は45分
単価は最安。対話には使えない
Serper$1.002,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件速い・50qpsSearXNGに直接挿せる唯一の選択
Perplexity Search API$5.00速い1リクエストに5クエリまとめられる=まとめれば実質$1
Exa$7.00登録時$20+$10/月=約1,428件/月速いDeep Search は $12〜15/1,000
Tavily$8.001,000credit/月速いAdvanced Searchは複数credit消費
Kagi Search API未確認なし速い公開docsから価格が消えた。要ログイン
SerpApi$25.00250件/月速い小規模では最も高い
Gemini Grounding($14/1,000)月5,000prompt無料速い超過後はprompt単位ではなく実際のquery単位で課金
2026-07-29 15:26 JST に全社の公式ページを再取得して確認しました。この時点で値上げ・値下げはありません。ただし DataForSEO は2026年7月1日に8つのAPIを約20%値上げしています(SERP APIは対象外だったため上表は有効)。API価格は動きます。半年経ったら測り直してください。

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/月〜小規模では最も高い
無料枠を「束ねる」のは規約上OKです。ただし同じサービスで複数アカウントを作って枠を増やすのはNG(Braveの利用規約に「複数アカウント作成を含む方法でrate limit/service limitを迂回してはならない」と明記)。異なる事業者を1アカウントずつ正規に使い分けるのは禁止されていません。

使えないもの(調べた結果)

サービス状況
Yahoo! JAPAN 検索API2013年に提供終了。現存するのはYOLP(地図・ローカル・気象)のみでウェブ検索APIは無い
Bing Search API2025年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
選択肢は年々減っています。Yahoo(2013)→Bing(2025)→Google CSE(2027予定)と、大手の公式検索APIが次々に閉じました。「今あるものが来年もある」と思わないほうがよく、1つに依存しない構成(今の3エンジン+無料枠3社)にしておく価値はここにあります

📌 残っていること

① チャットアプリはまだ SearXNG を使っていない

現在、チャットの検索は Serper(有料API)が使われています。

・7/25にSearXNGから切り替えたまま、戻していません。ラズパイを使っているのはニュースbotのcron(朝7時・夜19時)だけです。 戻すには src/routes/api/chat/+server.ts の SERPER_DISABLED = true。ユーザー方針で「テストで正しい結果が返ると確認できるまで戻さない」としています。

② Brave 公式APIキー(推奨)

Brave は今回いちばん結果品質が良かったエンジンです。問題は無料スクレイピングの予算だけ。公式APIキーを入れればIP評判とは無関係になります。SearXNG には braveapi という専用エンジンが標準搭載されており、settings.yml にコメントで設定を用意済みです。
APIで固められるのは実質 Brave だけです。Bing の公式検索APIは 2025年8月に廃止、SearXNG の Google CSE エンジンは公式APIではなく他人の設定を流用したもの、Mojeek・Yandex・DuckDuckGo には SearXNG 用のAPIエンジンがありません。

③「無制限・無料」についての正直な結論

一次情報を当たった結論として、家庭回線1本・スクレイピング中心で「回数無制限かつ安定」は2026年時点では成立しません。上流が許してくれないからです。
ただし Bing と Yandex は世界横断でエラー率0% です。「ブロックされないエンジンだけを使う」なら無料・無制限は今でも成立します。今回そこに寄せた結果が、上の6/6エラーゼロです。欲張って全エンジンを有効にすると、弱いエンジンが予算を焼いて全体を巻き添えにします。

用語集

用語意味このページでの出どころ
メタ検索自分では検索せず、複数の検索エンジンに代理で聞いて結果をまとめる仕組みSearXNG がこれ
スクレイピング人間向けWebページを機械で取得し、HTMLから必要な情報を抜き出すことSearXNGの基本動作
XPathHTMLの中の場所を指定する書き方。//ol[@id="b_results"] のような形bing.py が結果を抜くのに使う
robots.txtサイト所有者が「ここは機械で取らないで」と表明するファイル4社とも検索結果を拒否している
レートリミット一定時間に何回まで、という上限。超えると 429 Too Many RequestsBraveで35回ほどで到達
CAPTCHA「あなたはロボットですか」の確認。botと判定された合図DuckDuckGo・Qwantで発生
IP評判そのIPアドレスが過去にどう振る舞ったかの評価。共有IPだと他人の行いも影響する今回は無実だった
ファンアウト1つの要求が下流で複数の要求に増えること1検索→5クエリ→4エンジン=20リクエスト
サスペンド失敗したエンジンをしばらく呼ばないようにする仕組みsuspended_times
カバーリング(このページでは)予備の手段が本命の失敗を肩代わりすることDDG既定が落ちてもDDG webが返す
Tailscale Funnelルータに穴を開けずに自宅のサービスを外へ公開する仕組み。ドメイン不要アプリ→ラズパイの入口
CGNAT / MAP-E1つのIPv4を複数の契約者で共有する方式。他人の行いが自分に返る今回は該当しなかった

関連ページ