Djangoアプリ上でFessを使って検索窓を設置する

# はじめに
Djangoでサイトを作っていた際に、検索機能を手軽に設置したいなぁと思い、
OSSの検索エンジン `Fess` を使って実現したので方法を共有します。
Fessについて知りたい方は [こちら](https://fess.codelibs.org/ja/) をご参照ください。

# 想定読者
- Djangoでのアプリケーション開発手法を理解している
- Docker / docker-compose の最低限の使用法について理解している
- Nginxの最低限の設定方法について理解している

# 構成
docker-composeにて以下コンテナを起動します。

- `nginx`コンテナ
- `django`コンテナ
- `postgresql`コンテナ
- `fess`コンテナ
- `elasticsearch`コンテナ

以下は `docker-compose.yml` の例です。
なお、fessとelasticsearchの設定については[こちらのGithub](https://github.com/codelibs/docker-fess/tree/master/compose)を参考にさせていただきました。
また、ディレクトリ構成などは各開発状況に依存します。

```docker-compose.yml
version: '3.7'

services:
    nginx:
        image: nginx:1.19.3-alpine
        ports:
            - "80:8000"
        volumes:
            - ./nginx/conf:/etc/nginx/conf.d
            - ./nginx/uwsgi_params:/etc/nginx/uwsgi_params
            - ./nginx/log:/var/log/nginx
            - ./app/static:/static
        depends_on:
            - django

    django:
        build: ./app
        command: uwsgi --socket :8001 --module app.wsgi --py-autoreload 1
        volumes:
            - ./app/:/usr/src/app/
        ports:
            - 8001:8001
        depends_on:
            - postgres
            - fess

    postgres:
        image: postgres:12.4-alpine
        volumes:
            - ./postgres/data:/var/lib/postgresql/data

    fess:
        image: ghcr.io/codelibs/fess:13.12.0
        environment:
            - "ES_HTTP_URL=http://es:9200"
            - "FESS_DICTIONARY_PATH=/usr/share/elasticsearch/config/dictionary/"
        ports:
            - "8080:8080"
        depends_on:
            - es

    es:
        image: ghcr.io/codelibs/fess-elasticsearch:7.12.0
        environment:
            - node.name=es01
            - discovery.seed_hosts=es01
            - cluster.initial_master_nodes=es01
            - cluster.name=fess-es
            - bootstrap.memory_lock=true
            - "ES_JAVA_OPTS=-Xms1g -Xmx1g"
            - "FESS_DICTIONARY_PATH=/usr/share/elasticsearch/config/dictionary"
        ulimits:
            memlock:
              soft: -1
              hard: -1
            nofile:
              soft: 65535
              hard: 65535
        volumes:
            - ./elasticsearch/data:/usr/share/elasticsearch/data
            - ./elasticsearch/dictionary:/usr/share/elasticsearch/config/dictionary
        ports:
            - 9200:9200

```

ただし、これで `docker-compose up` すると 
「`elasticsearch` が立ち上がってないから `fess` が落ちたよ！」
と言われることがあります。

`depends_on`で指定はしていますが、内部の起動タイミングが合わないことがあるようです。

先にelasticsearchだけ立ち上げておけばよいのですが、よい解決法がありましたら教えてください。

# Djangoのテンプレートへの検索窓追加
[こちらのサンプル](https://fess.codelibs.org/ja/13.12/config/search-form.html)に従って検索窓をtemplateに配置します。
以下はサイトのURLが `https://yoursite.co.jp` の場合の例です。

```html
<form id="searchForm" method="get" action="https://yoursite.co.jp/search/">
    <input required pattern=".*\S+.*" id="query" type="text" name="q" maxlength="1000" autocomplete="off">
    <input type="submit" name="search" value="検索">
</form>
```

サンプルとの差分としては、`required pattern=".*\S+.*"` の部分です。
これがないと空、またはスペースのみの状態で検索ボタンが押せるのですが、
空で検索するとなぜか私の環境の場合はエラー画面となってしまったので追加しています。

# Nginxの設定ファイル修正

フロントからのリクエストパスに応じて振り分けていきます。
以下の例では、`/static`, `/admin/` などのDjangoのパスをまず設定し、
Fess関連のパスをfessコンテナに転送しています。
（とりあえず見つけた範囲を設定していますが他にもあるかもしれません）

最後にそれ以外のパスをdjangoコンテナに転送しています。
（Fess関連のパスがDjangoのパスと重複しないようにする必要があります）

```nginx.conf
upstream django {
    ip_hash;
    server django:8001;
}

server {
    listen      8000;
    server_name 127.0.0.1;
    charset     utf-8;

    location /static {
        alias /static;
    }

    location /admin/ {
         deny all;
    }

    location /search/ {
        proxy_pass http://fess:8080/search/;
    }

    location /css/ {
        proxy_pass http://fess:8080/css/;
    }

    location /js/ {
        proxy_pass http://fess:8080/js/;
    }

    location /images/ {
        proxy_pass http://fess:8080/images/;
    }

    location /go/ {
        proxy_pass http://fess:8080/go/;
    }

    location /cache/ {
        proxy_pass http://fess:8080/cache/;
    }

    location / {
        uwsgi_pass  django;
        include     /etc/nginx/uwsgi_params;
    }
}

server_tokens off;
```

# Fessの設定
ブラウザで `http://[IP address]:8080` にアクセスするとFessの画面が表示されます。
ログインすると管理画面が表示されますので、クロールの設定やスケジューラの設定をしてください。
詳細は[Fess公式サイト](https://fess.codelibs.org/ja/8.0/admin/webCrawlingConfig-guide.html)に記載されていますので、自分がハマったところだけ記載しておきます。

（以下はすごく素人的なハマりだと思いますので、熟練者の方は生暖かく見守りください）

Fessからのクロール先のURLは`localhost`では機能しません。
なぜなら、fessコンテナから見た`localhost`は、通常fessコンテナ自身を表すからです。

また、クロール先を `http://django:8001` や `http://nginx:8000` にすると一見インデックスが作成されているように見えます。
~~しかし、検索結果のリンクURLも上記アドレスとなってしまうため、検索結果からサイトにアクセスできません。~~
こちらコメントにて「パスマッピング」の設定をすればよいとのご指摘をいただきました！

なので、クロール先は `https://yoursite.co.jp` にしましょう。
この場合、クロールのリクエストは一旦外に出ることになります。
マシンのFW等でIP制限をしている場合は、マシン自体のグローバルIPを指定しないとはじかれてしまうので注意が必要です。

# その他はまりどころ
以下の設定（Ubuntu 20.04の例）をしないとelasticsearchコンテナが落ちます。

```.sh
$ sudo sysctl -w vm.max_map_count=262144
```