OpenSearch + Sudachi を 0 から構築する

形態素解析器 Sudachi の Elasticsearch プラグインである [elasticsearch-sudachi](https://github.com/WorksApplications/elasticsearch-sudachi) は、[OpenSearch](https://opensearch.org/) にも対応しています。

本記事ではこれを使って OpenSearch + Sudachi の環境を構築し、基本的な使い方を試します。

# 前提

- WSL (Ubuntu 22.04)
- OpenSearch v2.12.0
- `docker`, `java`, `curl` を使用します

# Docker で OpenSearch を立ち上げる

Docker で環境構築を行います。
公式ドキュメントの [Installing OpenSearch / Docker](https://opensearch.org/docs/latest/install-and-configure/install-opensearch/docker/) を参考にしています。

## Linux 側の初期設定

公式ドキュメントの指示に従い設定を変更します。

```bash
# 1. Disable memory paging and swapping performance on the host to improve performance.
sudo swapoff -a

# 2. Increase the number of memory maps available to OpenSearch.
# Edit the sysctl config file
sudo echo "vm.max_map_count=262144" >> /etc/sysctl.conf
# Reload the kernel parameters using sysctl
sudo sysctl -p
# Verify that the change was applied by checking the value
cat /proc/sys/vm/max_map_count
```

## Sudachi プラグインの準備

OpenSearch に elasticsearch-sudachi プラグインを追加するため、Dockerfile を作成します。

elasticsearch-sudachi に使用する OpenSearch のバージョンに対応するビルドがあれば、その URL を指定してインストールします（[elasticsearch-sudachi のリリースページ](https://github.com/WorksApplications/elasticsearch-sudachi/releases)）。

```Dockerfile:Dockerfile
FROM opensearchproject/opensearch:latest

# elasticsearch-sudachi:v3.1.0, openseaarch:v2.6.0 のビルドをインストールする例
RUN /usr/share/opensearch/bin/opensearch-plugin install --batch \
    https://github.com/WorksApplications/elasticsearch-sudachi/releases/download/v3.1.0/opensearch-2.6.0-analysis-sudachi-3.1.0.zip
```

対応するビルドが未リリースなどの場合は、自分でビルドしたものからインストールすることも可能です。

```bash
git clone https://github.com/WorksApplications/elasticsearch-sudachi.git
cd elasticsearch-sudachi

# OpenSearch (os), v2.12.0 でビルド
./gradlew -PengineVersion=os:2.12.0 build

# Dockerfile のある場所に持っていく
cp build/distributions/opensearch-2.12.0-analysis-sudachi-3.1.1-SNAPSHOT.zip /path/to/working/dir
```

```Dockerfile:Dockerfile
FROM opensearchproject/opensearch:2.12.0

# プラグインをファイルからインストールする
COPY opensearch-2.12.0-analysis-sudachi-3.1.1-SNAPSHOT.zip \
    opensearch-2.12.0-analysis-sudachi-3.1.1-SNAPSHOT.zip
RUN /usr/share/opensearch/bin/opensearch-plugin install --batch \
    file:opensearch-2.12.0-analysis-sudachi-3.1.1-SNAPSHOT.zip
```

さらに、Sudachi の実行には辞書ファイルが必要です。
[SudachiDict](https://github.com/WorksApplications/SudachiDict) からダウンロードし、これもイメージにコピーします。
なお elasticsearch-sudachi はデフォルトでは core 辞書を読み込みます。

```Dockerfile:Dockerfile
FROM opensearchproject/opensearch:2.12.0

# プラグインをファイルからインストールする
COPY opensearch-2.12.0-analysis-sudachi-3.1.1-SNAPSHOT.zip \
    opensearch-2.12.0-analysis-sudachi-3.1.1-SNAPSHOT.zip
RUN /usr/share/opensearch/bin/opensearch-plugin install --batch \
    file:opensearch-2.12.0-analysis-sudachi-3.1.1-SNAPSHOT.zip

# 辞書をコピーする（core 版）
COPY system_core.dic config/sudachi/system_core.dic
```

以上の内容の Dockerfile でイメージを作成します。
ここでは `opensearch-sudachi` という名前にしておきます。

```bash
docker build --tag=opensearch-sudachi .
```

## Docker compose でコンテナの立ち上げ

公式ドキュメントに習い、Docker Compose でコンテナを立ち上げできるようにします。

とはいえサンプルではノード二つ＋ダッシュボードの構成になっているので、以下には 1 ノードにしたものを記載します。

```yaml:docker-compose.yaml
services:
  opensearch: # This is also the hostname of the container within the Docker network (i.e. https://opensearch/)
    image: opensearch-sudachi # Specifying the latest available image - modify if you want a specific version
    container_name: opensearch
    environment:
      - cluster.name=opensearch-cluster # Name the cluster
      - node.name=opensearch # Name the node that will run in this container
      - discovery.seed_hosts=opensearch # Nodes to look for when discovering the cluster
      - cluster.initial_cluster_manager_nodes=opensearch # Nodes eligible to serve as cluster manager
      - bootstrap.memory_lock=true # Disable JVM heap memory swapping
      - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" # Set min and max JVM heap sizes to at least 50% of system RAM
      - OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD} # Sets the demo admin user password when using demo configuration, required for OpenSearch 2.12 and later
      - "DISABLE_INSTALL_DEMO_CONFIG=true" # Prevents execution of bundled demo script which installs demo certificates and security configurations to OpenSearch
      - "DISABLE_SECURITY_PLUGIN=true" # Disables Security plugin
    ulimits:
      memlock:
        soft: -1 # Set memlock to unlimited (no soft or hard limit)
        hard: -1
      nofile:
        soft: 65536 # Maximum number of open files for the opensearch user - set to at least 65536
        hard: 65536
    volumes:
      - opensearch-data:/usr/share/opensearch/data # Creates volume called opensearch-data1 and mounts it to the container
    ports:
      - 9200:9200 # REST API
      - 9600:9600 # Performance Analyzer
    networks:
      - opensearch-net # All of the containers will join the same Docker bridge network

volumes:
  opensearch-data:

networks:
  opensearch-net:
```

使用する Docker イメージを先ほどビルドした `opensearch-sudachi` に変更しています。
また実行を簡便にするため "DISABLE_SECURITY_PLUGIN=true" を設定していることに注意してください。



:::note warn
OpenSearch 2.12.0 より、admin パスワードの初期設定が必須とのことです。事前に以下の環境変数に設定しておいてください（パスワードの強度が低いと立ち上げ時にエラーとなります）。

```bash
export OPENSEARCH_INITIAL_ADMIN_PASSWORD="<strong-custom-password-for-admin>"
```
:::

コンテナを立ち上げ、Sudachi プラグイン (`analysis-sudachi`) がインストールされていることを確認します。

```bash
# コンテナ立ち上げ
docker compose up -d
docker ps -a

# プラグインの一覧を取得
curl -XGET "http://localhost:9200/_cat/plugins"
```

以上で OpenSearch + Sudachi が構築できました。

# Sudachi プラグインの機能を見る

続いて実際に Sudachi プラグインを動かしてみます。
セキュリティプラグインを切っているため、アクセスには http を用いることになります。

## 最小構成

最低限、Sudachi で解析を行うだけのアナライザーを定義します。

以下の設定ファイルを使ってインデックスを作成します。

```json:settings_minmum.json
{
  "settings": {
    "index": {
      "analysis": {
        "tokenizer": {
          "sudachi_tokenizer": {
            "type": "sudachi_tokenizer",
          }
        },
        "analyzer": {
          "sudachi_analyzer": {
            "tokenizer": "sudachi_tokenizer",
            "type": "custom"
          }
        }
      }
    }
  }
}
```

```bash
# index の作成
curl -XPUT "http://localhost:9200/index_minimum" -H "Content-Type: application/json" -d @settings_minimum.json

# 設定が反映されているか確認
curl -XGET "http://localhost:9200/index_minimum?pretty"
```

インデックスが作成できたら、適当な文章を解析させて動作を確認します。

```bash
# sudachi analyzer で解析させる
curl -XGET "http://localhost:9200/index_minimum/_analyze?pretty" -H 'Content-Type: application/json' -d' {"analyzer": "sudachi_analyzer", "text": "国会議事堂前駅で電車を降りた。"}'
```

テキストが単語に分割されていれば OK です。
特に、デフォルトでは C 単位で解析が行われるため、「国会議事堂前駅」がひとつの語になっているはずです。

## その他の機能

elasticsearch-sudachi には各種のフィルタが用意されています。
個別の説明は公式ドキュメントに譲りますが、例えば以下のような設定ができます（実用性を考慮したものではありません）。

```jsonc
{
  "settings": {
    "index": {
      "analysis": {
        "tokenizer": {
          "sudachi_tokenizer": {
            "type": "sudachi_tokenizer",
            // Sudachi 分割単位 (A/B/C)
            "split_mode": "A",
            // 句読点を無視するかどうか
            "discard_punctuation": true,
            // Sudachi 設定ファイルへのパス
            "settings_path": "/usr/share/opensearch/config/sudachi/sudachi.json",
            // 辞書を探索するディレクトリ
            "resources_path": "/usr/share/opensearch/config/sudachi",
            // 設定を json で直接記述 (settings_path を上書き)
            "additional_settings": "{\"systemDict\":\"system_full.dic\",\"userDict\":[\"user.dic\"]}"
          }
        },
        "filter": {
          // 指定した品詞/活用形を持つ語をスキップ
          "posfilter": {
            "type": "sudachi_part_of_speech",
            "stoptags": ["助詞", "助動詞", "補助記号,句点", "補助記号,読点"]
          },
          // 指定した語をスキップ
          "stopfilter": {
            "type": "sudachi_ja_stop",
            "stopwords": ["_japanese_", "は", "です"]
          },
          // 各語を読みに変換（ローマ字）
          "romaji_readingform": {
            "type": "sudachi_readingform",
            "use_romaji": true
          }
        },
        "analyzer": {
          "sudachi_analyzer": {
            "filter": [
              "sudachi_baseform", // 動詞と形容詞を終止形に変換
              "sudachi_normalizedform", // 各語を正規化形に変換
              "sudachi_readingform" // 各語を読みに変換（カタカナ）
            ],
            "tokenizer": "sudachi_tokenizer",
            "type": "custom"
          }
        }
      }
    }
  }
}
```

ここでは割愛しますが、前の例と同じようにインデックスを作成・アナライザーを適用することで挙動を確認することができます。

# データ投入と検索

最後にインデックスへドキュメントを投入し、検索を行ってみます。

## マッピングの設定と文書のインデキシング

投入する各ドキュメントを解析させるため、マッピングを設定します。
ここではドキュメント中の `body` 項目を解析させることにします。

合わせて上で触れた追加設定のうち、句読点を無視する `discard_punctuation` とストップワードを除去する `sudachi_ja_stop` フィルター（デフォルトのリストを使用）を設定してみます。

```json:settings_mapping.json
{
  "settings": {
    "index": {
      "analysis": {
        "tokenizer": {
          "sudachi_tokenizer": {
            "type": "sudachi_tokenizer",
            "discard_punctuation": true
          }
        },
        "analyzer": {
          "sudachi_analyzer": {
            "filter": ["sudachi_ja_stop"],
            "tokenizer": "sudachi_tokenizer",
            "type": "custom"
          }
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "body": {
        "type": "text",
        "analyzer": "sudachi_analyzer"
      }
    }
  }
}
```

投入するためのドキュメントを用意します。
bulk でまとめて投入するため、以下のようにファイルに列挙してしまいます。

```json:documents.jsonl
{"index": {}}
{"body": "日本の首都は東京都です。"}
{"index": {}}
{"body": "東京から京都まで行くなら、新幹線がいいと思います。"}
{"index": {}}
{"body": "私のふるさとは徳島ですから。"}
```

用意した設定で改めてインデックスを作成し、文書を投入します。

```bash
# index の作成
curl -XPUT "http://localhost:9200/index_sudachi" -H "Content-Type: application/json" -d @settings_mapping.json

# 文書の投入 (bulk では改行が必要なので `--data-binary` を使う)
curl -XPOST "http://localhost:9200/index_sudachi/_bulk" -H 'Content-Type: application/json' --data-binary @documents.jsonl

# 全件取得
curl -XGET "http://localhost:9200/index_sudachi/_search" -H 'Content-Type: application/json' -d '{"query":{"match_all":{}}}'
```

### 文書の検索

以上で文書を登録することができたので、検索クエリを投げてみます。

```bash
# "京都" で検索
curl -XGET "http://localhost:9200/index_sudachi/_search" -H 'Content-Type: application/json' -d '{"query": {"match": {"body": "京都"}}}'

# "ですから。" で検索
curl -XGET "http://localhost:9200/index_sudachi/_search" -H 'Content-Type: application/json' -d '{"query": {"match": {"body": "ですから。"}}}'
```

"京都" での検索では "東京から京都まで行くなら、新幹線がいいと思います。" のみがヒットするはずです。
一件目の "日本の首都は東京都です。" にも文字列 "京都" が含まれていますが、Sudachi の解析によって "東京都" にまとめて分割されているため、ヒットしません。

また "ですから。" での検索の方は、追加したフィルタによって語が除去されているためどの文書にもヒットしない結果となります。

# 終わりに

OpenSearch + Sudachi の環境構築から基本的な使い方までを試しました。

実用上は上に挙げた分割単位やフィルタを組み合わせることで、よりきめ細かな検索を実現することができます。
ぜひ試してみてください。