🍔

SearXNGでひたすらローカルから検索する

2025/02/27に公開

SearXNG、たぶん「サーチ・エックス・エヌ・ジー」みたいな読みかただと思うんですけど、これを使ってローカルからびしばし検索をします。

キーワード

  • メタ検索エンジン(SearXNG)
  • Docker
  • Python

課題

「検索エンジンをブラウザで開く」という行為との相性が悪い案件があり、ローカルから制限を気にせず検索できる、という世界観にしたいです。

調査

調べはじめると、kun432さんの記事がばあっと出てきて、「Perplexity」のクローンがあるよ、みたいなことをありがたく教わり、最終的にSearXNGの記事にたどりつきました。

概要

  1. Dockerを立ち上げる
  2. SearXNGのイメージを取り込む
  3. 設定をお好みにする
  4. バインドしたエンドポイントにパラメーターを叩き送る
  5. 出力結果(検索結果のJSONやCSV)を加工する

手続き

1. Dockerを立ち上げる

Docker Desktopを使うことでラクをします。

2. SearXNGのイメージを取り込む

たぶんいろんなやりかたがありますが、私はyamlファイル。

docker-compose.yml
services:
  searxng:
    image: searxng/searxng
    container_name: searxng
    ports:
      - "${PORT}:8080"
    volumes:
      - "./searxng:/etc/searxng"
    environment:
      - "BASE_URL=http://0.0.0.0:${PORT}/"
      - "INSTANCE_NAME=SEARXNG"

Dockerで起動させる流れやニュアンスは公式ドキュメントを参考。たとえば、公式ではこういう立ち上げかたをしています。

searxng公式
$ mkdir my-instance
$ cd my-instance
$ export PORT=8080
$ docker pull searxng/searxng
$ docker run --rm \
             -d -p ${PORT}:8080 \
             -v "${PWD}/searxng:/etc/searxng" \
             -e "BASE_URL=http://localhost:$PORT/" \
             -e "INSTANCE_NAME=my-instance" \
             searxng/searxng
2f998.... # container's ID

3. 設定をお好みにする

pullできたイメージのなかの設定ファイルをいじる。JSONで返ってくるようにお願いします。

searxng/settings.yml
search:
  formats:
    - html
+   - json

設定を変えたあとに、docker psでコンテナIDを調べて、docker restart {container_id}をします、たしか。

ちなみに、どこにバインドするかとかは、serverのところにあります。

searxng/settings.yml
server:
  port: 8888
  bind_address: "127.0.0.1"

公式ドキュメントが詳しいので、良さげな設定がないか探してみてください。 enginesとかsearchとかserverとかoutgoingあたりが匂いますね。

4. バインドしたエンドポイントにパラメーターを叩き送る

Seach APIで出来ることについては、公式ドキュメントだよりです。

curlとかでもいいんですけど、本件ではPythonだったのでPythonのrequestsパッケージを利用して飛ばすやつのデモやります。

main.py
import requests
import time
import random

# デモではまったくといっていいほど無意味ですが、まずは待つ姿勢を見せる
time.sleep(random.randint(3, 5))

# pagenoはページングの番号です
params = {
    "q": "メイド喫茶 おすすめ",
    "language": "ja",
    "pageno": 1,
    "engines": "google",
    "format": "json",
}
res = requests.get("http://127.0.0.1",params=params)
output_json = json.loads(res.text)

5. 出力結果(検索結果のJSONやCSV)を加工する

main.py - 続き
query = output_json["query"] # 検索したクエリが改めて見たい
results = output_json["results"] # 検索結果はここに格納されている

for index, r in enumerate(results):
    url = r["url"]
    title = r["title"]
    content = r["content"]
    engine = r["engine"] # "google", "duckduckgo"
    position = r["positions"] # 1,2,3,4,5...

ちょっとマシな全文

main.py
import sys
import requests
import time
import random
import json

SEARXNG_ENDPOINT = "http://127.0.0.1"

def main(params, engine="google", max_page=5):
    for pageno in range(1, max_page + 1):
        params["pageno"] = pageno

        try:
            time.sleep(random.randint(3, 5))
            res = requests.get(SEARXNG_ENDPOINT, params=params)
            if res.status_code != 200:
                print(f"リクエストエラー:ステータスコード {res.status_code}")
                return False
        except Exception as e:
            print(f"リクエストエラー:{e}")
            return False

        try:
            output_json = res.json()
            query = output_json["query"]
            results = output_json["results"]
        except Exception as e:
            print(f"検索結果のパースエラー:{e}")
            return False

        for r in results:
            print(f"検索結果:{r}")
            try:
                url = r["url"]
                title = r["title"]
                content = r["content"]
                engine = r["engine"]
                position = r["positions"]

                # ここで保存や加工など何かしらの処理
            except Exception as e:
                print(f"結果の処理エラー:{e}")

    return True

if __name__ == "__main__":
    count = 0
    while True:
        count += 1
        print(f"{count}回目のループ")

        # 検索クエリを毎回かえたいときはここで調整
        query = "メイド喫茶 おすすめ"

        # 検索パラメーター
        params = {
            "q": query,
            "language": "ja",
            "pageno": 1,
            "engines": "google",
            "format": "json",
        }

        try:
            if not main(params):
                print("スクリプトを停止します")
                sys.exit()
        except Exception as e:
            print(f"エラー: {e}")
            sys.exit()

余談:Pythonのマネージャー

いまは「uv」が人気急騰中でしょうか。手癖でrye initしがちですが、そろそろちゃんとuvに移行したいと思います。
(ryeの管理もuvのAstralに移行されたそうなので、、、)

まとめ

DuckDuckGoとは初めましてだったのですが、なんてことない顔して同梱されていました。個人的に検索したいデータは、どちらかというとGoogle検索よりはDuckDuckGoのほうにあったな、という感じです。

Torともいろいろできそうな感じが伝わってきたので、今度はTorとかTailscaleとかとからめた話もしたいですね。

案件募集中

お金ないので案件募集中です。課題を解決します(!)。

Discussion