こんにちは。インフラエンジニアの永井(shnagai)です
今回は3ヶ月ほど行っていたAWS OpenSearchの技術検証をしている中で、技術検証のスピードアップに貢献してくれたTIPSを2つご紹介出来ればと思います。
内容はざっくり下記2項目です。
- ユーザ辞書やシノニムOpenSearchのカスタムパッケージを使って管理する
- Reindex APIを使ったIndex変更内容の反映高速化
ユーザ辞書やシノニムOpenSearchのカスタムパッケージを使って管理する
カスタム辞書と日本語全文検索
今回、日本語の全文検索を行うための技術検証でOpenSearchを触りました。
全文検索を行うにあたっては、ユーザ辞書やシノニム、ストップワードの設定が肝になります。
例えば、OpenSearchで使える日本語の形態素解析エンジンであるkuromojiを使って文章を分割するケースを考えてみます。
ユーザ辞書を使わないプレーンなkuromojiの解析結果が下記です。
抱っこ/紐/と/朝/ごはん/を/食べる/こと
抱っこ紐が「抱っこ」と「紐」に分割されているので、このままだと「抱っこ紐」と全文検索した時にこの文章のスコアは高くなりません。(厳密に言うとsearch時にどう単語を分割して当てにいくかの話だがここでは単純なパターンを想定して話します)
「抱っこ紐」と検索したら、「抱っこ紐」の情報が一番に出てきてほしいので、ユーザ辞書で「抱っこ紐」を定義したインデックスを作ります。
tst_custom_dicというカスタム辞書を使ったインデックスのサンプル
PUT tst_custom_dic { "settings": { "index": { "analysis": { "tokenizer": { "kuromoji_user_dict": { "type": "kuromoji_tokenizer", "user_dictionary_rules": [ "抱っこ紐,抱っこ紐,ダッコヒモ,カスタム名詞" ] } }, "analyzer": { "tst_analyzer": { "type": "custom", "tokenizer": "kuromoji_user_dict" } } } } } }
user_dictionary_rules
部分でカスタム辞書を定義し、インデックスを再作成することで下記のように「抱っこ紐」を一単語として分割することが可能になります。
抱っこ紐/と/朝/ごはん/を/食べる/こと
カスタムパッケージを利用してインデックスを定義
先程説明した形は、インデックスのtokenizerの定義で辞書を管理する方法なのですが、カスタム辞書は何百、何千というワードを登録するためインデックスの定義で辞書を管理すると本質的ではない部分で定義が冗長になり可読性や保守性が著しく下がってしまいます。
このような課題を解決するために、OpenSearchではs3に保存したカスタム辞書ファイルを使える機能がカスタムパッケージという名前で提供されています。 シノニムとストップワードも同じ方法で外部ファイル化が可能です。
詳細な設定方法は、下記の公式ブログで詳しく解説されているのでこちらをご覧ください。
https://docs.aws.amazon.com/ja_jp/opensearch-service/latest/developerguide/custom-packages.html
カスタムパッケージを使う際のインデックスの定義は下記のようになります。
PUT tst_custom_dic { "settings": { "index": { "analysis": { "tokenizer": { "kuromoji_user_dict": { "type": "kuromoji_tokenizer", "user_dictionary": "analyzers/F72444002" } }, "analyzer": { "tst_analyzer": { "type": "custom", "tokenizer": "kuromoji_user_dict" } } } } } }
tokenizer内の user_dictionary
に analyzers/パッケージID
を指定することで利用可能になります。
カスタムパッケージを使うことで、辞書更新が下記のようなフローで可能になります。
- カスタム辞書を手元のエディタで追加
- s3にファイルをアップロード
- OpenSearchのカスタムパッケージ機能で2のファイルをインポート
- 新しいversionが付与される
- 辞書更新をしたいOpenSearchクラスタで「更新を適用」するとパッケージが最新になる(5分くらいかかるがその間は前のverを使える)
- 同じカスタムパッケージを利用していても、クラスタ単位で適用タイミングをずらせるので、stgで先にカスタム辞書を更新して動作確認してから、本番にも適用するというようなフローを組むことも出来ます。
- インデックスを更新(Reindexもしくは作り直し)することで新たなカスタム辞書でトークン分割されたインデックスを利用可能に
- Reindexについては、この後紹介します
- エイリアス更新するかインデックス名を変えて参照元からの参照先を変えるか
- この辺はまだ設計途上
今考えているデプロイパターンだと、
2のs3アップロードをGitHubマージトリガで動かして、3以降はs3のputトリガでStepFunctionsを動かすも良し、CIツールでAWS SDK使ったフロー組むのも良しという形で自由度高く組めると考えています。
まだ、その部分の検証はしていないので手を動かしながら最適解を探していきたいと思います。
最後に、もっとこうなるとうれしいと思った部分を一つだけ
- パッケージの更新時にバリデーション走ると尚うれしい
- 現状だと辞書の内容の間違いに気づくのは、openSearchのインデックスを更新するタイミングでのエラーで気づく
- 只、自動適用考える時にCI側でバリデーションするのがフローとしては健全と思っている(この部分はまだ未検証)
Reindex APIを使ったIndex変更内容の反映高速化
OpenSearch(Elasticsearchでも一緒)では辞書の更新はもちろん、インデックス自体の定義を更新するには、インデックス自体を新規で作り直す必要があります。
これはインデックスの作成時に、定義に基づいてトークン化が行われるからです。
検証中は特に、インデックスの定義を更新して、データがどう変わるかを見るというオペレーションが頻発します。
ストレートにこのインデックス更新を行うと、下記手順を繰り返します。
- 定義の更新
- 元のインデックスの削除
DELETE Index
- インデックスの新規作成
PUT Index ~BodyにJSONの定義
- 新規のインデックスにデータを投入
あまりにも非効率だなと思い、色々と調べるとelastic社の公式ドキュメントにReindexというAPIが紹介されていることを発見しました。
https://www.elastic.co/guide/en/elasticsearch/reference/7.6/docs-reindex.html
下記のような簡単なAPIを叩くことで、インデックスのコピーが出来ることを発見しました。
量により処理時間はマチマチですがbulkでデータ投入するよりは圧倒的に早いです。
POST _reindex { "source": { "index": "元のインデックス名" }, "dest": { "index": "新しいインデックス名" } }
このReindexを使うようになったことで技術検証のスピードは圧倒的に早くなりました。(始めから気づけよという話ではあるのですが。。)
Reindexを使った本格的な運用について、二三歩先に進んだ事例も下記のブログで紹介されています。 https://tech.legalforce.co.jp/entry/2021/12/21/190129
今回はOpenSearch(Elasticsearch)の運用に役立ちそうなTIPSを2つ紹介しました。
次回はもう少し踏み込んだ内容の日本語の形態素解析でハマったことを中心に書こうと思います。
コネヒトではサービスの信頼性向上をミッションに幅広い領域をカバーしながらエンジニアリングの力でサービスをよりよくしていけるエンジニアを募集しています。 少しでも興味もたれた方は、是非気軽にお話出来るとうれしいです。