shnagaiの日記

主にエンジニアリング関連のトピックについて雑多に書いています

AWS OpenSearchでkuromojiのユーザ辞書の検証をする手順

最近検索エンジン周りの検証をしていて、AWS OpenSearchと戯れていました。

余談ですが、昔ログ基盤としてElasticsearchをそこそこヘビーユースしていた時代もあったのですが、今回n年ぶりに触っていたら色々と忘れていたり進化していたりで触ってないと忘れるなっていうのを身を以て体験しました。

今回は小ネタで、OpenSearchでkuromojiの辞書整備をどんな感じでやっていったかを書き記しておこうと思います。 ※個人ブログの更新は実に2年ぶり!!

この記事はコネヒト Advent Calendar 2021の17日目の記事です。

OpenSearchで検証してる時の個人的な感想

  • OpenSearch自体は公式ドキュメントはしっかりしているのだが、webで検索しても中々いい記事にたどり着けないというのが色々と調べていたときの感想。
  • Elasticsearchの7.1をforkして作られているという話なので、使い方みたいな部分はElasticsearch7.1以降の記事を参考にするとOpenSearchでやりたいことに近づけたという印象(あくまで個人の印象)
  • 特に下記の@johtaniさんのブログには助けられました。

blog.johtani.info

blog.johtani.info

OpenSearch Dashboard

OpenSearchにはKibanaライクなOpenSearch DashboardというGUI管理画面があります。

CURL経由でJSONを投げつけての検証は中々辛いものがあるので、今回はこちらのDevToolsを使ってkuromojiのユーザ辞書の検証を行いました。 ※kibanaのDev Toolsとほぼ一緒の使い勝手です。

f:id:nagais:20211217164511p:plain

ユーザ辞書でやりたいことのおさらい

ユーザ辞書でやりたいことはシンプルで、トークナイザーの標準辞書だとうまくあたらないワードをユーザ辞書を使うことで当てることです。

例えば、「抱っこ紐」と検索した時に、標準のkuromoji_tokenizer だと「抱っこ」「紐」に単語分割されトークン化されてしまうため、「抱っこ」「紐」それぞれにマッチする文章がヒットしてしまいます。これを「抱っこ紐」というユーザ辞書を登録することで単語分割時に「抱っこ紐」というトークンにすることで、「抱っこ紐」というワードにヒットする文章を検索することが可能になります。

下記のelastic社のブログで詳しく解説されていますが、検索エンジンを考える上で、再現率(検索モレが少ない)と適合率(ノイズが少ない)のトレードオフが大事になってきますが、今回の辞書のアプローチはノイズをへらすために辞書を整備するというアプローチになります。

すべてを辞書で賄うことは出来ないので、形態素解析でノイズを減らしつつ、n-gramで検索モレを減らしていくアプローチが良さそうです。(検索の専門家ではないのでこのあたりは色々試行錯誤しながらベストなパターンを見つけていきたいです)

www.elastic.co

kuromojiユーザ辞書を検証しながら作っていく手順

今回の検索エンジンの検証では、日本語を扱う必要があったのでkuromojiを使いました。 ※OpenSearchはsudachiは現時点ではサポートされていません。

ここから、kuromojiのユーザ辞書を整備していくために今回取った手順を説明します。 簡単にいうと、kuromojiの標準トークナイザーで該当単語をがどうトークン化されるかを調べて、意図せぬ分割がされていたら辞書化するというアプローチを取りました。

①kuromojiの標準トークナイザーでどのようなトークン化されるかを確認する

## 標準辞書のkuromoji_tokenizerで引く
GET _analyze
{
  "tokenizer": "kuromoji_tokenizer",
  "char_filter": {
    "type": "icu_normalizer",
    "name": "nfkc",
    "mode": "compose"
  },
  "filter": [
    "kuromoji_part_of_speech",
    "kuromoji_stemmer"
  ],
  "text": "抱っこ紐で生後6ヶ月の赤ちゃん"
}

結果: 「抱っこ紐」が「抱っこ」「紐」だったり、「生後6ヶ月」が「生後」「6」「ヶ月」に分割されています。

{
  "tokens" : [
    {
      "token" : "抱っこ",
      "start_offset" : 0,
      "end_offset" : 3,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "紐",
      "start_offset" : 3,
      "end_offset" : 4,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "生後",
      "start_offset" : 5,
      "end_offset" : 7,
      "type" : "word",
      "position" : 3
    },
    {
      "token" : "6",
      "start_offset" : 7,
      "end_offset" : 8,
      "type" : "word",
      "position" : 4
    },
    {
      "token" : "ヶ月",
      "start_offset" : 8,
      "end_offset" : 10,
      "type" : "word",
      "position" : 5
    },
    {
      "token" : "赤ちゃん",
      "start_offset" : 11,
      "end_offset" : 15,
      "type" : "word",
      "position" : 7
    }
  ]
}

②ユーザ辞書を使ってanalyzerを登録するために、擬似的なdemo_custom_dicというインデックスを作成

  • user_dictionary_rulesでユーザ辞書を定義しています
  • ユーザ辞書はインデックス作成時に登録されるので、ワードを追加するときは一度 DELETE demo_custom_dic でインデックスを消してからサイドインデックス登録するという手順を繰り返します。
PUT demo_custom_dic
{
  "settings": {
    "index": {
      "analysis": {
        "tokenizer": {
          "kuromoji_user_dict": {
            "type": "kuromoji_tokenizer",
            "mode": "search",
            "discard_compound_token": true,
            "user_dictionary_rules": [
              "朝ごはん,朝ごはん,アサゴハン,カスタム名詞",
              "抱っこ紐,抱っこ紐,ダッコヒモ,カスタム名詞",
              "生後6ヶ月,生後10週,セイゴジュッシュウ,カスタム名詞"
            ]
          }
        },
        "analyzer": {
          "demo_analyzer": {
            "type": "custom",
            "tokenizer": "kuromoji_user_dict",
            "char_filter": {
              "type": "icu_normalizer",
              "name": "nfkc",
              "mode": "compose"
            },
            "filter": [
              "kuromoji_part_of_speech",
              "kuromoji_stemmer"
            ]
          }
        }
      }
    }
  }
}

③demo_custom_dicインデックスに登録したdemo_analyzerを使ってどのようにトークン化されるか確認

  • _analyzer APIを使うことでインデックスに実際にドキュメントを登録せずとも辞書を使ったトークン分割を確認出来るので便利です。
## ユーザ辞書を登録したkuromoji_tokenizerで引く
GET demo_custom_dic/_analyze
{
  "analyzer": "demo_analyzer",
  "text": "抱っこ紐で生後6ヶ月の赤ちゃん"
}

結果: 「抱っこ紐」が「抱っこ紐」だったり、「生後6ヶ月」が「生後6ヶ月」にとユーザ辞書がトークン化時に考慮されて意図した分割がされています。

{
  "tokens" : [
    {
      "token" : "抱っこ紐",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "生後6ヶ月",
      "start_offset" : 5,
      "end_offset" : 10,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : "赤ちゃん",
      "start_offset" : 11,
      "end_offset" : 15,
      "type" : "word",
      "position" : 4
    }
  ]
}

このようにして実際にどのようにトークン化されるかを確認しながらユーザ辞書を整備しています。 ある程度の辞書が出来たら、OpenSearchではパッケージという仕組みを使ってs3に保管したファイルをユーザ辞書やシノニム、ストップワードに使うことが出来るのでそちらを使って管理するのがよさそうです。

こちらも検証した感じ下記ドキュメントの通りにやれば簡単に出来ました。

docs.aws.amazon.com