Rustで検索エンジンを作ってみた

## 作ってみたよ
Rust, Next.js, ElasticSearchで検索エンジンを作ってみました。

作ってみたものはこちら。
https://dev-search.com/
現状、入ってるのはほとんどstackoverflowのデータなので英語で検索してください。

## なにをするものか？
開発に関する情報を検索するための検索エンジンです。開発に関する情報を色々取得…しているつもりですが今はデータの割合はほとんどstackoverflowのデータとなっています。

## こんなに大量にどこからデータを取ってきたの？
気をつけてほしいのはこんなに大量のデータをクローリングで取ろうとすると普通にスパム行為になっちゃうことです。他人のサイトをクローリングする際にはやり方によってはサーバーに大きな影響を与えてしまうということに気をつけましょう。

ではどうやってデータを取得したかというと、Googleでstackoverflow dumpで検索すると色々な場所にデータがおいてあることがわかります。

私はこれをつかいました。
https://archive.org/details/stackexchange

最新のデータはStack Exchange APIで取っています。

なお、クローリングをする際は以下のことを守りましょう。
1. 必ずrobot.txtを守るようにクローラーを作ること
1. robot.txtでクローリングを拒否しているサイトはクローリングを諦めましょう。
1. アクセスの合間にはsleepを入れて適切な間隔を置きましょう。図書館のシステムをクローリングして訴えられた人もいるので、だいぶ長めに○分単位の間隔でもいいかも。
1. API経由でデータを取得する場合、backoffでアクセス間隔を指定されます。必ず守りましょう。

## 苦労したこと
### ハッカーがしつこ過ぎる
当初公開した際、アクセスログをみると
```
35.203.211.8 - - [30/Dec/2024:05:15:37 +0000] "\x00\x00\x00\xAC\xFESMB..."
```
みたいなハッキング目的であろうアクセスログが大量にありました。しかも0.1秒間隔みたいに全くサーバーの負荷を考えないアクセスをしてきます。そのせいで当初はかなりサーバーが重い状態でした。

まあIPブロックをすればいいんですが、向こうもプロなので大量にIPアドレスをもっています。手動でブロックするのも現実的ではありません。

### 解決方法
解決方法ですが、まず悪意のある評判の悪いIPアドレスはネットで無料公開されています。
例えばこれ。
https://github.com/stamparm/ipsum

こういったものを有効活用しましょう。

またfail2banもおすすめです。これはアクセスログを自動解析し、自動で怪しいIPアドレスをブロックしてくれます。

どういうパターンのアクセスをブロックするのか
```conf
[Definition]
failregex = ^<HOST> - - .*SMB.*
ignoreregex =
```

どういう条件でブロックするのか
```conf
[nginx-malicious]
enabled = true
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 300
bantime = 600
```
を細かく指定できます。
なお、Dockerでnginx/apacheを使っている場合は以下に注意です。普通にfail2banを有効にするだけだとブロックできてない可能性があります。
https://github.com/fail2ban/fail2ban/issues/2376#issuecomment-2565534465

あとRedisを導入して、rate limiterも実装しておきましょう。Rate limiterはAPIを提供するアプリ側でもたいていできるはずですし、Nginxでもできます。

### 頑張ってキャッシュしよう
どんなサイトにも言えますが、キャッシュをうまく使うと、大幅にサーバー負荷を減らすことができパフォーマンスは向上しますし、サーバー代も減りますしいいことづくめです。

見過ごしがちですが、必ず可能な限りキャッシュしましょう。

- Nginxを使ってstatus: 200のGetのレスポンスを（getクエリ含めた）url単位でキャッシュしましょう。サーバーサイドの処理をしないので当然ですが応答速度が大きく向上します。getクエリごとキャッシュするのは、Getクエリが`?id=5`と`?id=10`の場合はそれぞれレスポンスがちがうので同じ内容をキャッシュしないためです。ただし、他のユーザに見えちゃまずい内容（クレカ番号とか）は必ずサーバー側でチェックを通してから表示すべきなのでキャッシュしないよう注意。あと400系とか500系もキャッシュしないように注意。
- 上記のキャッシュはcloudflareでも可能です(CDNキャッシュ)。cloudflareはさらにWAFとしての機能も持ってるのでぜひ導入しましょう。
- Redisによるアプリ側のキャッシュを活用しましょう。重い処理は一度実行したら実行結果をしばらくRedisに入れておいて次回以降はそちらを参照すると処理の負荷が減ります。
- ブラウザ側のキャッシュを可能な限り使いましょう。Nginxやサーバーサイドのアプリ側でキャッシュ用のヘッダをつけて調整しましょう。この記事( https://qiita.com/koheiyamaguchi0203/items/48cec8c03014c61bca4c )はすごく参考になります。

### Adsenseを設定してるのに表示されない 😥
Google AdSenseを設定しているにもかかわらず広告が表示されないことが多々ありました。

考えられる原因は 2 つです。
1. ウェブサイトの評価（レピュテーション）が低い
2. 指定した広告サイズに対して広告が存在しない（あるいはマッチしない）

1の原因はすぐには変えられませんが、2の原因に対しては以下のように対策してみました。

### 対策

最初は大きすぎないサイズの固定広告を設置していました。
```jsx
<GoogleAdUnit>
    <ins class="adsbygoogle"
        style="display:inline-block;width:300px;height:90px"
        data-ad-client="ca-pub-{ad-client-id}"
        data-ad-slot="{slot id}">
    </ins>
</GoogleAdUnit>
```
しかし、これだと広告が表示されないことが多かったです（Next.js では nextjs13_google_adsense を使っています）。

そこで、レスポンシブ広告を試してみました。デフォルトのレスポンシブ広告コードは次の通りです。
```jsx
<GoogleAdUnit>
    <ins
        className="adsbygoogle"
        style={{ display: 'block', width: '100%' }}
        data-ad-client="ca-pub-{ad-client-id}" // AdSense の Client ID
        data-ad-slot="{slot id}"               // Ad slot ID
        data-ad-format="auto"
        data-full-width-responsive="true"
    />
</GoogleAdUnit>
```
これは広告のサイズが自動調整されるため、他のサイズ制限がなく柔軟ですが、私の感覚では広告が大きすぎると感じました。

そこで、以下のように `height`を絞り、さらに`data-ad-format="horizontal"`を指定することで横長広告に限定しました。
```jsx
<GoogleAdUnit>
    <ins
        className="adsbygoogle"
        style={{ display: 'block', width: '100%', height: '50px' }}
        data-ad-client="ca-pub-{ad-client-id}"
        data-ad-slot="{slot id}"
        data-ad-format="horizontal"
        data-full-width-responsive="true"
    />
</GoogleAdUnit>
```
この変更により、時々表示されないことはあるものの、かなりの頻度で広告が出るようになりました。幅の制限がほぼないため、広告枠に合うものが表示されやすいようです。

ただ、ひとつ気づいたのが、コンソールに`Uncaught TagError: adsbygoogle.push() error: All ins elements in the DOM with class=adsbygoogle already have ads in them.`というエラーが…。しかたないので最終的にこうしました。

```jsx
    React.useEffect(() => {
        try {
            (window.adsbygoogle = window.adsbygoogle || []).push({});
        } catch (err) {
            console.error(err);
        }
    }, [pathname, searchParams]);

    return (
        <ins
        className="adsbygoogle"
        style={{ display: 'block', width: '100%', height: '50px' }}
        data-ad-client="ca-pub-{ad-client-id}"
        data-ad-slot="{slot id}"
        data-ad-format="horizontal"
        data-full-width-responsive="true"
        />
    );
```

## 最後に
まあとりあえず動くものができてよかったです。