日本語に適したインデックスの作成

BLOG

こんにちは。
サイオステクノロジーの田川です。

前回、登録した「桃太郎」の内容を検索しようとしましたが、 うまく検索できないことがありました。
主な原因は、作成したインデックスが日本語用に設定されていなかったことにあります。

今回は、日本語に適したインデックスを作成してみます。

※今回は、過去4回よりは、多少、難しい内容になっています。

対象者

  • Elastic Cloud のアカウントを持っている人(トライアルライセンスを含む)
  • Elasticsearch の初心者~中級者

できるようになること

  • Elastic Cloud のデプロイメントに日本語用の形態素解析に必要なプラグインを追加インストールする。
  • Elastic Cloud のデプロイメント内に日本語に適したインデックスを作成する。
  • reindex を用いて、旧インデックスから新インデックスへのドキュメントのコピーを行う。
  • 日本語を含むキーワード検索を行う。

前提条件

  • Elastic Cloud (version: 8.15.0)
  • Elastic Cloud 上にデプロイメントを作成済

(2024年10月15日時点の情報を元に記載しています。)

1. プラグインの追加インストール

日本語に適した検索を行えるようにするため、日本語の形態素解析を行えるようにします。

日本語の形態素解析の例です。

おばあさんは川へ洗濯に行きました



おばあさん / は / 川 / へ / 洗濯 / に / 行きました

このように形態素解析することで、日本語の検索を行いやすくなります。

日本語の形態素解析を行えるようにするため、
Elastic Cloud のデプロイメントに対し、必要なプラグインの追加インストールを行います。

  1. Elastic Cloud へログインします。
  2. Elastic Cloud のデプロイメント(このブログ記事では”SIOS-BLOG-SAMPLE-1″)の Manage をクリックします。
    deployment-manage
  3. 左のメニューから Edit をクリック
    deployment-edit
  4. Manage user settings and extensions をクリック
    manage-user-settings-and-ext-1
  5. Elasticsearch user settings and extensions のダイアログが表示されます。
    ここで、Extensions のタブをクリックします。
    manage-user-settings-and-ext-2
  6. 追加可能なプラグインの一覧が表示されます。

ここから、

  • analysis-icu
  • analysis-kuromoji

の2つにチェックを入れて、ダイアログ下部の [Back] をクリックします。
(この状態では、2つのプラグインは、まだ追加されていません。)
icu-and-kuromoji

  1. ダイアログが閉じられた後、画面下部の [Save] をクリックします。
    save
  2. 次のような確認ダイアログが表示されるので、[Confirm] をクリックします。
    confirm
    [Confirm] をクリックすると、2つのプラグインの追加インストールが開始されます。
  3. しばらく待つと、プラグインのインストールが完了し、次のような画面になります。
    activity-list

これで、日本語の形態素解析を行える準備ができました。

2. 日本語用のインデックスの作成準備

日本語の文章を検索するためのインデックスの作成準備を行います。

設定する項目の値を下表に記載しておきます。

項目設定値備考
インデックス名momotaro_v2
number_of_shards1あくまでもサンプルなので 1 としておく。
number_of_replicas1あくまでもサンプルなので 1 としておく。
refresh_interval3600s
analyzerja_kuromoji_index_analyzer登録用アナライザー(*1)
analyzerja_kuromoji_search_analyzer検索用アナライザー(*1)
char_filterja_normalizer(*2)
char_filterkuromoji_iteration_mark「々」、「ヽ」、「ゝ」 などの踊り字の正規化する。“各々”→”各各”
tokenizerja_kuromoji_tokenizer(*3)
user_dictionary_rules定義する“きびだんご”と”鬼が島”を登録しておく。(*4)
filterkuromoji_baseform原形化“来ました”→”来る”
filterkuromoji_part_of_speech不要な品詞を除去する。“東京の天気は晴れ”→”東京”,”天気”,”晴れ”
filtercjk_width全角英数記号→半角英数記号、半角カナ→全角カナ“#A1カナ”→”#A1カナ”
filterja_stopストップワードの除去“この街角”→”街角”
filterkuromoji_number漢数字の半角数字化“一二三四”→”1234”
filterkuromoji_stemmer長音の除去“サーバー”→”サーバ”

kuromoji_tokenizer などの詳細については、Elastic社の公式ドキュメントを参照してください。
https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-icu.html
https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html
など

(*1) 今回の設定内容では、登録用のアナライザーと検索用のアナライザーは同じなので、兼用することも可能ですが、将来、検索用のアナライザーにのみ同義語を登録する場合もあるため、登録用と検索用のアナライザーは別にしておくことをお薦めします。

(*2) ja_normalizer の詳細設定は下記になります。

項目説明
typeicu_normalizerElasticsearchで提供されている char_filter .
namenfkc_cf正規分解して合成する。ただし合成文字は分解する。また、小文字への統一も行う。“㌢”→”セ”,”ン”,”チ”, “APPLE”→”apple”
modecompose分解された文字を合成する。“か” + “゛” → “が”

(*3) ja_kuromoji_tokenizer の詳細設定は下記になります。

項目説明
typekuromoji_tokenizerトークン化を行う。“東京国際空港”→”東京”,”東京国際空港”,”国際”,”空港”
modesearch検索を対象としたセグメンテーションで単語分割する。“ファンクションコール”→”ファンクション”,”コール”
discard_compound_tokentruemode=searchのときに複合語が出力されなくなる。“東京国際空港”→”東京”,”国際”,”空港”

(*4) 本来は、何をユーザー辞書に登録すべきか?を調査すべきですが、今回は、その手順を省略しています。後からユーザー辞書に単語を追加登録することは可能ですが、その場合ドキュメントの再登録が必要となります。(後からユーザー辞書に単語を追加登録しても、既にインデックスされたドキュメントに対しては影響を与えないため)

※上記以外に同義語登録もありますが、ひとまず、考慮しないでおきます。

(なお、検索用の同義語であれば後から追加しても、ドキュメントの再登録は不要です。)

※その他、N-gramを考慮して検索を行うこともありますが、今回は触れないでおきます。


3. 日本語用のインデックスの作成

前項で掲載した値を踏まえて、日本語用のインデックスを新規に作成します(インデックス名= “/momotaro_v2” )。

PUT /momotaro_v2
{
  "settings": {
    "index": {
      "number_of_shards": 1,
      "number_of_replicas": 1,
      "refresh_interval": "3600s"
    },
    "analysis": {
      "char_filter": {
        "ja_normalizer": {
          "type": "icu_normalizer",
          "name": "nfkc_cf",
          "mode": "compose"
        }
      },
      "tokenizer": {
        "ja_kuromoji_tokenizer": {
          "mode": "search",
          "type": "kuromoji_tokenizer",
          "discard_compound_token": true,
          "user_dictionary_rules": [
            "きびだんご,きびだんご,キビダンゴ,カスタム名詞",
            "鬼が島,鬼が島,オニガシマ,カスタム名詞"
          ]
        }
      },
      "analyzer": {
        "ja_kuromoji_index_analyzer": {
          "type": "custom",
          "char_filter": [
            "ja_normalizer",
            "kuromoji_iteration_mark"
          ],
          "tokenizer": "ja_kuromoji_tokenizer",
          "filter": [
            "kuromoji_baseform",
            "kuromoji_part_of_speech",
            "cjk_width",
            "ja_stop",
            "kuromoji_number",
            "kuromoji_stemmer"
          ]
        },
        "ja_kuromoji_search_analyzer": {
          "type": "custom",
          "char_filter": [
            "ja_normalizer",
            "kuromoji_iteration_mark"
          ],
          "tokenizer": "ja_kuromoji_tokenizer",
          "filter": [
            "kuromoji_baseform",
            "kuromoji_part_of_speech",
            "cjk_width",
            "ja_stop",
            "kuromoji_number",
            "kuromoji_stemmer"
          ]
        }
      }
    }
  }
}

content フィールドの登録時に “ja_kuromoji_index_analyzer” を適用し、
content フィールドに対する検索時に “ja_kuromoji_search_analyzer” を適用するように設定します。

PUT /momotaro_v2/_mappings
{
  "dynamic": false,
  "properties": {
    "chunk_no": {
      "type": "integer"     
    },
    "content": {
      "type": "text",
      "analyzer": "ja_kuromoji_index_analyzer",
      "search_analyzer": "ja_kuromoji_search_analyzer"
    }
  }
}

これで、ドキュメントを登録する準備はできました。

4. ドキュメントのコピー(Reindex)

Reindex API を使って、
事前に momotaro インデックスに登録しておいた内容を、
新たに momotaro_v2 インデックスへコピーします。

momotaro_v2 インデックスへドキュメントが登録される際に、上記で設定しておいた
analyzer (ja_kuromoji_index_analyzer) が適用され、適切な形態素解析が行われます。

reindex 終了時に refresh を行い、検索可能な状態にするよう refresh=true を付けておきます。
(今回のインデックスでは、リフレッシュ間隔を1時間としているため)

POST /_reindex?refresh=true
{
  "source": {
    "index": "momotaro"
  },
  "dest": {
    "index": "momotaro_v2"
  }
}

Reindex API の詳細については、下記を参照してください。
https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html

なお、momotaro インデックスにデータを登録していない場合は、
momotaro_v2 インデックスへ bulk API を使って、データを登録してください。

これで momotaro_v2 インデックスにドキュメントが登録され、日本語での検索を行えるようになりました。

5. 検索してみる。

前回うまく検索できなかった”桃太郎”を検索してみます。

検索リクエスト:

GET /momotaro_v2/_search
{
  "query": {
    "match": {
      "content": "桃太郎"
    }
  }
}

レスポンス:

{
  ...
  "hits": {
    "total": {
      "value": 31,
      "relation": "eq"
    },
    "max_score": 2.3206909,
    "hits": [
      {
        "_index": "momotaro_v2",
        "_id": "*******************",
        "_score": 2.3206909,
        "_source": {
          "chunk_no": 75,
          "content": "「桃太郎さん、桃太郎さん、どちらへおいでになります。」"
        }
      },
      ...
    ]
  }
}

ちゃんと31件ヒットします。

(日本語用に設定されていない momotaro インデックスでは、40件でした。)

“桃太郎” または “きびだんご” を含むドキュメントを検索してみます。

検索リクエスト:

GET /momotaro_v2/_search
{
  "query": {
    "match": {
      "content": "桃太郎 きびだんご"
    }
  }
}

レスポンス:

{
  ...
  "hits": {
    "total": {
      "value": 36,
      "relation": "eq"
    },
    "max_score": 4.487536,
    "hits": [
      {
        "_index": "momotaro_v2",
        "_id": "****************",
        "_score": 4.487536,
        "_source": {
          "chunk_no": 62,
          "content": " きびだんごがうまそうにでき上がると、桃太郎のしたくもすっかりでき上がりました。"
        }
      },
      ...
    ]
  }
}

36件がヒットします。

(日本語用に設定されていない momotaro インデックスでは、108件でした。)

“桃太郎” と “きびだんご” の両方を含むドキュメントを検索してみます。

検索リクエスト:

GET /momotaro_v2/_search
{
  "query": {
    "match": {
      "content" : {
        "query" : "桃太郎 きびだんご",
        "operator": "and"
      }
    }
  }
}

レスポンス:

{
  ...
  "hits": {
    "total": {
      "value": 5,
      "relation": "eq"
    },
    "max_score": 4.487536,
    "hits": [
      {
        "_index": "momotaro_v2",
        "_id": "*******************",
        "_score": 4.487536,
        "_source": {
          "chunk_no": 62,
          "content": " きびだんごがうまそうにでき上がると、桃太郎のしたくもすっかりでき上がりました。"
        }
      },
      ...
    ]
  }
}

5件がヒットしました。

“桃太郎”は含むが”きびだんご”を含まないドキュメントを検索してみます。

検索リクエスト:

GET /momotaro_v2/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "content" : "桃太郎"
          }
        }
      ],
      "must_not": [
        {
          "match": {
            "content": "きびだんご"
          }
        }
      ]
    } 
  }
}

レスポンス:

{
  ...
  "hits": {
    "total": {
      "value": 26,
      "relation": "eq"
    },
    "max_score": 2.3206909,
    "hits": [
      {
        "_index": "momotaro_v2",
        "_id": "Qv6NLJIBnwZx8PFpcU2K",
        "_score": 2.3206909,
        "_source": {
          "chunk_no": 75,
          "content": "「桃太郎さん、桃太郎さん、どちらへおいでになります。」"
        }
      },
      ...
    ]
  }
}

26件がヒットしました。

形態素解析の効果も確かめてみます。

検索リクエスト:

GET /momotaro_v2/_search
{
  "query": {
    "match": {
      "content" : "三にんの家来が桃太郎からもらったもの"
    }
  }
}

レスポンス:

...
   "content": " 桃太郎と、三にんの家来は、さっそく、この船に乗り込みました。"
...
   "content": " 犬と、猿と、きじと、これで三にんまで、いい家来ができたので、桃太郎は..."
...
   "content": " 犬はきびだんごを一つもらって、桃太郎のあとから、ついて行きました。"
...

どのような形態素解析が行われているのか、確かめてみます。

リクエスト:

GET /momotaro_v2/_analyze
{
  "analyzer": "ja_kuromoji_search_analyzer",
  "text": "三にんの家来が桃太郎からもらったもの"
}

レスポンス:

{
  "tokens": [
    {
      "token": "3",
      "start_offset": 0,
      "end_offset": 1,
      "type": "word",
      "position": 3
    },
    {
      "token": "家来",
      "start_offset": 4,
      "end_offset": 6,
      "type": "word",
      "position": 7
    },
    {
      "token": "桃太郎",
      "start_offset": 7,
      "end_offset": 10,
      "type": "word",
      "position": 9
    },
    {
      "token": "もらう",
      "start_offset": 12,
      "end_offset": 15,
      "type": "word",
      "position": 11
    }
  ]
}

つまり、”三にんの家来が桃太郎からもらったもの” の検索は、
“3”, “家来”, “桃太郎”, “もらう”
のいずれかを含むドキュメントの検索に置き換わっています。

  • “三” は、”3″ に変換されています。
    (検索対象のドキュメントには検索用キーワードとして”三”ではなく、”3″が登録されています。
    検索用クエリ内の”三”も”3″に変換してから検索を行います。)
  • “の”, “が”, “から” が不要な品詞と判断され除去されています。
  • “にん” も除去されていますが、これは、”に” と “ん” が不要な品詞と判断されているためだと思われます。
  • “もの” はストップワードとして除去されています(あるいは、”も” と “の” が不要な品詞と判断され削除されています)。
  • “もらった” が “もらう” に変換されています。。

このように、特別な形態素解析の処理を独自に実装しなくても、あらかじめ設定を行っておくことで Elasticsearch 内で自動的に形態素解析を行ってくれます。

今回は、日本語に適したインデックスを作成し、日本語でのキーワード検索ができるようになりました。
次回は、ベクトル検索について触れたいと思います。

※参考URL
https://www.elastic.co/jp/blog/how-to-implement-japanese-full-text-search-in-elasticsearch