pythonでOpensearchの検索機能を扱う

# はじめに

本記事はpythonでopensearchにデータを挿入、そして取得する一連の流れを簡単にまとめたものです
pythonを中心にopensearchを勉強しているので個人的に体系化したいと思ったのが動機です
pythonでopensearchを扱う上でのざっくりとした流れをつかんでいただければ幸いです


#作業環境
Dockerを利用しOpenSearchとOpenSearch Dashboardsを使いました
その際には```kuromoji```を扱えるようにしました

#データを用意

今回挿入するデータは以下のものを架空の本をサンプルとして用意しました
各カラムは以下のような内容です。
id　            : 一意なid
bookName        :本の名前
description     :本の内容
genre           :ジャンル
publicationDate :出版日

```csv:bookdata.csv
id,bookName,description,genre,publicationDate
a0001,pythonを学ぶ,python初心者向けの本です,プログラミング,2023-12-24
a0002,Go100選,GOしっかりと学びたい人向けです,プログラミング,2022-12-31
a0003,今年の冬はこれだ!,冬のイベント情報が詰まっています,旅行,2025-11-21
a0004,今年の秋はこれだ!,秋のイベント情報が詰まっています,旅行,2023-08-21
a0005,世界128の秘境,世界の128の秘境が画像とともに紹介されています,旅行,2027-02-21
a0006,日本の秘境６選,日本の秘境６つに焦点をあてて細く解説していきます,旅行,2022-04-03
a0007,三丁目の水野さん,三丁目の水野さんは八百屋さんにいくそうです,小説,2021-12-24
a0008,三丁目の水野さん2,三丁目の水野さんは京都に行くそうです,小説,2022-12-24
a0009,三丁目の水野さん3,三丁目の水野さんはイタリアにいくそうです,小説,2023-12-24
a0010,ライゼ旅物語,ベルエルン国で生まれたライゼの旅物語,小説,2025-03-01
a0011,水の美味しい飲み方,水の美味しい飲み方を紹介します,生活,2023-02-03
a0012,水の美味しい飲み方2,水の美味しい飲み方を紹介します,生活,2023-03-03
```


## opensearchにデータを挿入する

全体的なコードは以下のようになりました


```python:insert.py
# opensearchpyモジュールについて
import csv
from opensearchpy import OpenSearch
from setting import settings

# 接続先について
host = "localhost"
port = 9200
auth = ("admin", "admin")
client = OpenSearch(
    hosts=[{"host": host, "port": port}],
    http_compress=True,
    http_auth=auth,
    use_ssl=True,
    verify_certs=False,
)

# indexの作成とindexの中のデータに対しての設定
index_name = "bookinfo"
if not client.indices.exists(index=index_name):
    client.indices.create(index=index_name,body=settings)

# データの挿入
id = 1
with open("bookdata.csv","r",encoding="utf-8") as f:
    reader = csv.DictReader(f)
    for data in reader:
        client.index(
            index = index_name,
            body = data,
            id = id
        )
        id += 1
```

### opensearchpyモジュールについて
pythonでopensearchにアクセスできるようになります
インストールが必要です
下記が使用したopensearch-pyです
https://github.com/opensearch-project/opensearch-py

### 接続先について
上のコードでは`client`が接続先情報になります
接続先等の書き方は下記を参考にしました
色々な設定ができるみたいです
https://opensearch.org/docs/latest/clients/python/

#### indexの作成とindexの中のデータに対しての設定

```python:insert.py(1)
index_name = "bookinfo"
if not client.indices.exists(index=index_name):
    client.indices.create(index=index_name,body=settings)
```

indexの名前は```bookinfo```としました
このindexが存在しないときに作成するようにしました
bodyにはindexの中のデータの設定を書きます
下記のような設定をしmappingします



```python:setting.py
settings = {
  "mappings": {
    "properties": {
      "id":{
        "type": "keyword"
      },   
      "bookName":{
        "type": "text",
        "analyzer": "kuromoji"
      },
      "description":{
        "type": "text",
        "analyzer": "kuromoji"
      },      
      "publicationDate":{
        "type": "date",
      } 
    }
  }
}
```


indexの中のデータに対しての設定です
typeはデータの型を設定します
text,int,float,date,object等あります
詳しくは下記にのっています
https://opensearch.org/docs/latest/search-plugins/sql/datatypes/
またanalayzerに```kuromoji```を設定することで日本語検索が可能になります


このmappingはインデックスを作成するタイミングで行われるため,mappingを更新したい場合は一回indexを削除します

```python
client.indices.delete(index="bookinfo")
```

### 挿入されたデータを確認
Opensearch Dashboardsで確認します
できてそうです
![dash2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1430891/2984ea3e-c722-346a-4662-4961d9f07e3e.png)


# opensearchに検索をかける

検索する際の例をいくつか考えました
全体的なコードは以下の通りです


```python:search_book.py
from opensearchpy import OpenSearch
import json
host = "localhost"
port = 9200
auth = ("admin", "admin")
client = OpenSearch(
    hosts=[{"host": host, "port": port}],
    http_compress=True,
    http_auth=auth,
    use_ssl=True,
    verify_certs=False,
)


# ひとつのフィールドで検索
def search_book1(index_name: str,q: str):
    query ={
        "query":{
            "match":{"description":q}
        }
    }
    result = client.search(index=index_name, body=json.dumps(query))
    print(result)
    return result
search_book1("bookinfo","三丁目")

# 複数フィールドで検索
def search_book2(index_name: str,q: str, start: int, size: int):
    query = {
        "query": {
            "multi_match": {
                "fields": [
                    "bookName",
                    "description",
                    "genre"
                ],
                "query": q,
            }
        },
        "from": start,
        "size": size,
    }

    result = client.search(index=index_name, body=json.dumps(query))
    print(result)
    return result

search_book2("bookinfo","三丁目",1,5)



# idで完全一致で検索
def search_book_detail(index_name: str, id: str):
    query = {
    "query": {"term": {"id": id}},
    "_source": [
        "bookName",
        "description",
        "genre",
        "publicationDate"
    ],
    }
    result = client.search(index=index_name, body=json.dumps(query))
    print(result)

search_book_detail("bookinfo","a0001")


#　日付を降順で取得
def get_new_book(index_name : str):
    query = {
        "query": {"match_all": {}},
        "sort": [{"publicationDate": {"order": "desc"}}],
        "size": 5,
    }
    result = client.search(index=index_name, body=json.dumps(query))
    print(result)
    return result

get_new_book("bookinfo")
```

以下の部分で検索してデータを取得しています


```python:search_book.py(1)
client.search(index=index_name, body=json.dumps(query))
```

### ひとつのフィールドで検索
一番シンプルな検索です
ひとつのフィールドに対して検索します
mappingで`bookName`,`genre`,`description`をtypeを`text`としました
またanalyzerを`kuromoji`としたので日本語検索ができるようになっています
なのでひらがな一文字で検索などしてもヒットしないようになっています

### 複数のフィールドで検索
複数のフィールドにわたって検索する方法です
`fields`に検索したいフィールドを入れます

また,`from`や`size`を設定することで取得数などを調整できます


from : ヒットしたうち何件目から取得するか　(書いていない場合は1件目から取得します)
size : ヒットしたうち何件取得するか　(書いていない場合は10件取得します)

### idで完全一致で検索
id等のユニークなものの検索はこれが適していると思います
mappingで`keyword`として設定することで可能です
完全一致なものを取得します
`_source`には取得したいフィールドを設定できます

### 日付を降順で取得
mappingでtypeを`date`としています
`sort`を使うことで取得するデータに対してソートできます
また`match_all`は全件取得したい際につかいます


# 取得できたデータ
上記のコードを実行すると例えば`search_book1("bookinfo","三丁目")`では
下記のような結果が得られます

```
{'took': 3, 'timed_out': False, '_shards': {'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0}, 'hits': {'total': {'value': 3, 'relation': 'eq'}, 'max_score': 2.39394, 'hits': [{'_index': 'bookinfo', '_type': '_doc', '_id': '8', '_score': 2.39394, '_source': {'id': 'a0008', 'bookName': '三丁目の水野さん2', 'description': '三丁目の水野さんは京都に行くそうです', 'genre': '小説', 'publicationDate': '2022-12-24'}}, {'_index': 'bookinfo', '_type': '_doc', '_id': '9', '_score': 2.39394, '_source': {'id': 'a0009', 'bookName': '三丁目の水野さん3', 'description': '三丁目の水野さんはイタリアにいくそうです', 'genre': '小説', 'publicationDate': '2023-12-24'}}, {'_index': 'bookinfo', '_type': '_doc', '_id': '7', '_score': 2.2460306, '_source': {'id': 'a0007', 'bookName': '三丁目の水野さん', 'description': '三丁目の水野さんは八百屋さんにいくそうです', 'genre': '小説', 'publicationDate': '2021-12-24'}}]}}

```

この取得できたデータに処理をくわえればいろいろ活用できそうです

# おわり
opensearchに対してpythonで処理をする場合の一連の簡単な流れをまとめました
上記以外にも細かい検索だったりエイリアスの設定だったりいろいろありそうです
個人的にgithubのコードに書かれている説明等もわかりやすかったので見ていきたいと考えています
https://github.com/opensearch-project/opensearch-py


ここまで読んでいただきありがとうございました

# 参照
https://opensearch.org/docs/latest/clients/python/
https://github.com/opensearch-project/opensearch-py
https://opensearch.org/docs/latest