たったの1時間で構築?! Docker 環境で Opensearch の機能を触ってみる

## はじめに
この記事では、`Docker`を使用して`OpenSearch`をサクッと構築する方法を紹介します。

### 対象
* 学習やデモ環境として`Opensearch`を触ってみたい方
* `Docker`を使ったことがある方
* `AWS`で構築するのが面倒だと感じる方

### 環境
- **Python**: 3.11.0  
- **Docker**: インストール済  

## OpenSearch の環境構築

### 必要ファイルの準備
以下の公式リンクから `docker-compose.yml` ファイルを取得します。  
[OpenSearch公式ダウンロードページ](https://opensearch.org/downloads.html#:~:text=Download%20docker%2Dcompose.yml%20into%20your%20desired%20directory)

中身は以下を参照

### docker-compose.yml
以下は、2ノード構成の`OpenSearch`クラスタと`OpenSearch Dashboards`の設定です。

```yaml
version: '3'
services:
  opensearch-node1:
    image: opensearchproject/opensearch:latest
    container_name: opensearch-node1
    environment:
      - cluster.name=opensearch-cluster
      - node.name=opensearch-node1
      - discovery.seed_hosts=opensearch-node1,opensearch-node2
      - cluster.initial_cluster_manager_nodes=opensearch-node1,opensearch-node2
      - bootstrap.memory_lock=true
      - OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m
      - OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD}
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - opensearch-data1:/usr/share/opensearch/data
    ports:
      - 9200:9200
      - 9600:9600
    networks:
      - opensearch-net
  opensearch-node2:
    image: opensearchproject/opensearch:latest
    container_name: opensearch-node2
    environment:
      - cluster.name=opensearch-cluster
      - node.name=opensearch-node2
      - discovery.seed_hosts=opensearch-node1,opensearch-node2
      - cluster.initial_cluster_manager_nodes=opensearch-node1,opensearch-node2
      - bootstrap.memory_lock=true
      - OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m
      - OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD}
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - opensearch-data2:/usr/share/opensearch/data
    networks:
      - opensearch-net
  opensearch-dashboards:
    image: opensearchproject/opensearch-dashboards:latest
    container_name: opensearch-dashboards
    ports:
      - 5601:5601
    expose:
      - '5601'
    environment:
      OPENSEARCH_HOSTS: '["https://opensearch-node1:9200","https://opensearch-node2:9200"]'
    networks:
      - opensearch-net

volumes:
  opensearch-data1:
  opensearch-data2:

networks:
  opensearch-net:
```

---

### OpenSearch とは
以下公式より引用 (DeepL翻訳)
> **OpenSearch**は、Apache Luceneをベースにした分散型検索・分析エンジンです。データを追加することで、全文検索やフィールドによる検索、複数インデックスの検索、スコアによるランク付け、結果の集計などが可能です。

主に以下の用途で使われていることが多いです
* ログ・イベントデータの収集・分析
* フルテキスト検索
* データ分析・可視化
* 機械学習・異常検知
* 分散データストレージ(ビックデータの保存)


### OpenSearch Dashboards とは
以下公式より引用 (DeepL翻訳)
> **OpenSearch Dashboards**は、OpenSearchのデフォルトのデータ可視化ツールであり、セキュリティ、アラート、インデックス管理など多くの機能を提供します。

主に以下の用途で使われていることが多いです
* ログの可視化
* アラート・監視システムの構築
* 検索クエリの実行・データ探索
* メンバー間のダッシュボードの共有

---

### OpenSearch をローカル上で構築

まず、環境変数のパスワードを定義します。
```zsh
$ export OPENSEARCH_INITIAL_ADMIN_PASSWORD=<password>
```

:::note warn
パスワードの強度には気をつけてください。
簡単な文字列ですと、コンテナが立ち上がりません。
```zsh
Password hoge is weak. Please re-try with a stronger password.
```
:::

以下のコマンドを実行して、`OpenSearch`クラスタを起動します。

```zsh
$ docker-compose up -d
```

以下のコマンドを実行して、コンテナが立ち上がっているか確認します。


```zsh
$ docker compose ps

NAME                    IMAGE                                            COMMAND                  SERVICE                 CREATED         STATUS         PORTS
opensearch-dashboards   opensearchproject/opensearch-dashboards:latest   "./opensearch-dashbo…"   opensearch-dashboards   7 minutes ago   Up 7 minutes   0.0.0.0:5601->5601/tcp
opensearch-node1        opensearchproject/opensearch:latest              "./opensearch-docker…"   opensearch-node1        7 minutes ago   Up 7 minutes   0.0.0.0:9200->9200/tcp, 9300/tcp, 0.0.0.0:9600->9600/tcp, 9650/tcp
opensearch-node2        opensearchproject/opensearch:latest              "./opensearch-docker…"   opensearch-node2        7 minutes ago   Up 7 minutes   9200/tcp, 9300/tcp, 9600/tcp, 9650/tcp
```

`OpenSearch Dashboards` にアクセスするには、ブラウザで以下のURLを開いてください。
ログイン画面が表示されると思います。

* [http://localhost:5601/app/home](http://localhost:5601/app/home)

![スクリーンショット 2025-01-30 21.11.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680905/2479da0a-b5a1-b22e-0dba-0e8e8672cc1c.png)

おめでとうございます! `OpenSearch` の構築は完了です。

## Python で OpenSearch を操作

以下は `Python` を使った `OpenSearch` の基本操作例です。

### ライブラリ
* `opensearch-py`

https://github.com/opensearch-project/opensearch-py/tree/main

以下公式より引用 (DeepL翻訳)
> OpenSearch 低レベル Python クライアント (opensearch-py) は OpenSearch REST API のラッパーメソッドを提供し、Python でクラスタとより自然にやりとりできるようにします。 指定した URL に生の HTTP リクエストを送信するのではなく、クラスタ用の OpenSearch クライアントを作成し、クライアントの組み込み関数を呼び出すことができます。

### インストール
以下のコマンドを実行してください。
```zsh
$ pip install opensearch-py
```

### Python スクリプトを実装

```python
import os
from opensearchpy import OpenSearch


def main() -> None:
    host = "localhost"
    port = 9200
    auth = (
        "admin",
        os.getenv("OPENSEARCH_INITIAL_ADMIN_PASSWORD"),
    )
    
    # クライアントを作成
    client = OpenSearch(
        hosts=[{"host": host, "port": port}],
        http_auth=auth,
        use_ssl=True,
        verify_certs=False,
        ssl_show_warn=False,
    )

    index_name = "japanese-folktales"

    # インデックスを作成
    if not client.indices.exists(index=index_name):
        client.indices.create(index=index_name)
    else:
        query = {"query": {"match_all": {}}}
        client.delete_by_query(index=index_name, body=query)

    folktales = [
        {
            "name": "桃太郎",
            "story": "桃川上から流れてきた大きな桃から生まれた桃太郎が、犬・猿・きじを家来にして、鬼を討伐する",
            "attributes": ["正義感", "チームワーク", "勇気"],
        },
        {
            "name": "浦島太郎",
            "story": "浦島太郎は、亀を助けたことで竜宮城へ招かれ、そこで時の流れを忘れる。しかし、地上に戻ると時が大きく過ぎていて驚く。",
            "attributes": ["弱いものを守る", "約束", "玉手箱"],
        },
        {
            "name": "かぐや姫",
            "story": "竹の中から現れた美しい女性、かぐや姫は多くの求婚者を試しふるいにかけ、最終的には月に帰る。",
            "attributes": ["知的", "お金と権力", "結婚"],
        },
        {
            "name": "一寸法師",
            "story": "一寸法師は非常に小さな男の子で、大小の武器を使って大きな冒険を繰り広げる。最終的には巨大な鬼を倒す。",
            "attributes": ["お椀の舟", "機転", "打ち出の小槌"],
        },
        {
            "name": "金太郎",
            "story": "赤い服を着た力持ちの金太郎は、山の動物たちと毎日楽しく過ごしていた。最終的にはお偉いさんの家来となる。",
            "attributes": ["強い", "急ぐな休むな", "まさかり"],
        }
    ]

    print('\nデータを格納 ... \n')

    # インデックスにデータを格納
    for folktale in folktales:
        response = client.index(index=index_name, body=folktale)
        print(f"id: {response['_id']}, name: {folktale['name']}")

    query = {
        "query": {
            "match": {
                "story": "鬼"
            }
        }
    }

    # インデックスのデータを検索
    search_response = client.search(index=index_name, body=query)
    print("\nデータを検索 ... \n")
    print("鬼が登場する話:")
    for hit in search_response['hits']['hits']:
        print(f"* {hit['_source']['name']}")


if __name__ == "__main__":
    main()
```

---
### スクリプトを実行

```zsh
$ python3 opensearch_script.py

データを格納 ... 
 
id: diSVuZQBTfZX6cbTp4Px, name: 桃太郎
id: dySVuZQBTfZX6cbTqIMD, name: 浦島太郎
id: eCSVuZQBTfZX6cbTqIMV, name: かぐや姫
id: eSSVuZQBTfZX6cbTqIMp, name: 一寸法師
id: eiSVuZQBTfZX6cbTqIM9, name: 金太郎

データを検索... 

鬼が登場する話: 
* 桃太郎
* 一寸法師
```

### Opensearch Dashboards を確認
ブラウザで以下のURLを開いてください。

* [http://localhost:5601/app/dev_tools#/console](http://localhost:5601/app/dev_tools#/console)

ログインが完了している場合、`DevTools` の `Console` が表示されると思います。
この画面でスクリプトと、同じクエリを実行してみましょう。

![スクリーンショット 2025-01-31 8.48.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680905/17d07201-57ad-076c-7e19-346824a3081f.png)

期待通りの検索結果が返ってきたら、成功です! おめでとうございます!

## これからの提案
以下を検証して、さらに`OpenSearch` の理解を深めるのはいかかでしょう。

* クライアントの作成方法を変えてみる
* `Amazon OpenSearch Service` で構築してみる
* 大量データの投入 & クエリ実行時間の計測

## おわりに

この記事では、`Docker`を使用して `OpenSearch` をサクッと構築する方法を紹介しました。

( タイトルの「1時間」は筆者が実際に試して要した時間です。環境により前後する可能性がありますのでご了承ください。 )

この基盤を使って`OpenSearch`を用いた、より発展的なデータ検索や分析をお試し頂けると幸いです。


## エンジニア募集
私の所属する日本システム技研では、webエンジニアを募集しています。

興味のある方は、ぜひ求人情報をご覧ください。

https://job.mynavi.jp/26/pc/search/corp201129/outline.html

## 参照

https://aws.amazon.com/jp/opensearch-service/

https://github.com/opensearch-project/opensearch-py/tree/main/samples

https://opensearch.org/docs/latest/