kubernetes上のelasticsearchにユーザ辞書が欲しい

#はじめに
自宅のおうちkubernetes上のelasticsearchでは、elastic社が提供しているkuromojiプラグインを利用しています。
このプラグインは辞書が古く「クラウド」などの語は全くの未対応なので、ユーザ辞書で対応させました。

#前提環境
以下の記事で構築した環境です。elasticsearchは都度最新に更新しています。

https://qiita.com/Y-Shikase/items/f12271df70099e14834f

#ユーザ辞書の定義
kubernetesっぽくスマートに実装する事を考えた結果、ConfigMapで辞書ファイルを管理する事にしました。
ConfigMapでもディレクトリにvolumeMountする方法であれば、サービスを再起動させずとも動的にPodに反映されます。
また、永続ストレージよりも容易に利用できて、同じ内容を複数のPodで参照する事もできます。

##configmapの作成
Podから参照できるファイル名は「userdict.txt」としています。
注意点なのですが、ConfigMapの大きさはデフォルトで3MBまでです。少し調べましたが、変更はできるようなのですが、kubernetes全体のパフォーマンスにも影響があるようなので、ユーザ辞書の登録語が非常に多い方は永続ストレージを準備した方が無難でしょう。

```yaml:elastic-dict.yaml
apiVersion: v1
data:
  userdict.txt: |
    東京スカイツリー,東京 スカイツリー,トウキョウ スカイツリー,カスタム名詞
kind: ConfigMap
metadata:
  name: elastic-dict
  namespace: elastic
```

```shell-session:
# kubectl apply -f elastic-dict.yaml
```

##elasticsearchコンテナへのmount

elasticsearchのマニフェストファイルの前提は、以下の記事になります。ここのstatefulsetのyamlを編集します。

https://qiita.com/Y-Shikase/items/f12271df70099e14834f#statefulset%E3%81%A7%E3%81%AE3%E3%83%8E%E3%83%BC%E3%83%89%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%BF%E3%81%AE%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4basic%E8%AA%8D%E8%A8%BC%E3%81%82%E3%82%8A%E7%89%88

コンテナへvolumeMountsする際は、ディレクトリ指定でマウントするようにしてください。
subPathでファイル単体を既存ディレクトリにマウントした場合、ConfigMap(ユーザー辞書)を更新してもPod内に更新内容が反映されません。

```yaml:elasticsearch-sts-sec.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: elasticsearch
  namespace: elastic
spec:
  selector:
    matchLabels:
      app: elasticsearch
  serviceName: "elasticsearch"
  replicas: 3
  template:
    metadata:
      labels:
        app: elasticsearch
    spec:
      initContainers:
      # 略
      containers:
      - name: elasticsearch
        image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
        # 略
        volumeMounts:
# volumeMountsにelastic-dictをmountする定義を追記
        - name: elastic-dict
          mountPath: /usr/share/elasticsearch/config/dict
      volumes:
# volumesにelastic-dictのConfigMapを追記
        - name: elastic-dict
          configMap:
            name: elastic-dict
    # 略
```

ビビりなので、一旦deleteで完全に停止させてから、applyします。(applyの意味ない...)

```shell-session:
# kubectl delete -f elasticsearch-sts-sec.yaml
# kubectl apply -f elasticsearch-sts-sec.yaml
```

#ユーザ辞書の更新
ちゃんとPodにマウントできているか確認します。

```shell-session:
# kubectl exec -it elasticsearch-0 -n elastic -- cat /usr/share/elasticsearch/config/dict/userdict.txt
東京スカイツリー,東京 スカイツリー,トウキョウ スカイツリー,カスタム名詞
```

ConfigMapリソースなので、kubectlのeditで更新します。

```shell-session:
# kubectl edit configmap/elastic-dict -n elastic
```

viでの編集画面になるので、yamlに単語を追加します。
「クラウド」をカスタム名詞で追加しました。

```yaml:
# Please edit the object below. Lines beginning with a '#' will be ignored,
# and an empty file will abort the edit. If an error occurs while saving this file will be
# reopened with the relevant failures.
#
apiVersion: v1
data:
  userdict.txt: |
    東京スカイツリー,東京 スカイツリー,トウキョウ スカイツリー,カスタム名詞
    クラウド,クラウド,クラウド,カスタム名詞
kind: ConfigMap
metadata:
  creationTimestamp: "2021-03-25T14:35:02Z"
  name: elastic-dict
  namespace: elastic
  resourceVersion: "79233490"
  uid: bf3f976a-d279-41ec-bd1c-b4f264fd5b16
```

「:wq」で保存終了すると、「configmap/elastic-dict edited」とメッセージが出ます。

Podから内容が変更されているか確認します。
(kubernetesのPodに反映されるまで最大で120秒かかります。)

```shell-session:
# kubectl exec -it elasticsearch-0 -n elastic -- cat /usr/share/elasticsearch/config/dict/userdict.txt
東京スカイツリー,東京 スカイツリー,トウキョウ スカイツリー,カスタム名詞
クラウド,クラウド,クラウド,カスタム名詞
```

Podからユーザ辞書の更新が確認できたら、ユーザ辞書の読み直しをするためにインデックスのクローズをして、再度オープンします。

```shell-session:
# curl -XPOST -u elastic http://elasticsearch:9200/testindex/_close?pretty
# curl -XPOST -u elastic http://elasticsearch:9200/testindex/_open?pretty
```

解析済みのフィールドを持ったドキュメントを既に持っている場合、更新されたユーザ辞書で再度解析させるには、「_update_by_query」で全ドキュメントを指定します。

```shell-session:
# curl -H 'Content-Type: application/json' -XPOST -u elastic http://elasticsearch:9200/testindex/_update_by_query?conflicts=proceed\&pretty -d '
{ 
  "query": {
    "match_all": {}
  }                
}'
```

ここまでやっててアレですが、ユーザ辞書の更新にkubernetesのAPIを使う事になるので、外部ツールで連携させづらい事に気づきました。
ですが、ユーザ辞書の「更新」は頻度が低いと思うので私はこの方法で管理しようと思います。

#おまけ　〜解析器での確認〜
記載が前後しますが、以下のようなユーザ辞書を使うだけのアナライザを設定したインデックスを作成して確認しています。

```shell-session:
# curl -H 'Content-Type: application/json' -XPUT -u elastic http://elasticsearch:9200/testindex?pretty -d '
{
  "settings": {
  "index": {
      "analysis": {
        "tokenizer": {
          "custom_tokenizer": {
            "type": "kuromoji_tokenizer",
            "mode": "search",
            "discard_punctuation": "true",
            "user_dictionary": "dict/userdict.txt"
          }
        },
        "analyzer": {
          "custom_analyzer": {
            "type": "custom",
            "tokenizer": "custom_tokenizer"
          }
        }
      }
    }
  }
}'
```

アナライザを利用した確認は以下のようなコマンドです。

```shell-session:
# curl -H 'Content-Type: application/json' -XPOST -u elastic http://elasticsearch:9200/testindex/_analyze?pretty -d '
{
  "analyzer": "custom_analyzer",
  "text": ["東京スカイツリーの上部にクラウドが"]
}'
```

結果は以下になります。

```json:
{
  "tokens" : [
    {
      "token" : "東京",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "スカイツリー",
      "start_offset" : 2,
      "end_offset" : 8,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "の",
      "start_offset" : 8,
      "end_offset" : 9,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : "上部",
      "start_offset" : 9,
      "end_offset" : 11,
      "type" : "word",
      "position" : 3
    },
    {
      "token" : "に",
      "start_offset" : 11,
      "end_offset" : 12,
      "type" : "word",
      "position" : 4
    },
    {
      "token" : "クラウド",
      "start_offset" : 12,
      "end_offset" : 16,
      "type" : "word",
      "position" : 5
    },
    {
      "token" : "が",
      "start_offset" : 16,
      "end_offset" : 17,
      "type" : "word",
      "position" : 6
    }
  ]
}
```

ちゃんと「クラウド」で解析されています。

#おわりに
構築してわかったのですが、頻繁にユーザ辞書を更新する用途には向かないですね。
あと、ConfigMapの更新が遅い時は遅いので、次の作業のインデックスへの反映のタイミングがとりづらいです。
ですが、3MBの制限はあるもののkubernetes内で「ファイルを持たなくて良い」というのは魅力的な運用方法かなと思ってます。

たまに私含めelasticsearchで悩んでいる方の呟きに応答してくださるOhtani様、ありがとうございます！！

https://twitter.com/johtani/status/1321009634035912705

※本記事とOhtani氏は無関係です。