Elasticsearch 入門。その3

2021.08.24

Elasticsearch 初学者の中村です。

前回はElasticsearchで検索する方法を学びました。 今回は検索するためにElasticsearchがどのようにデータを管理しているか、転置インデックスやマッピング、アナライザについて書いてみます。

その2はこちら

転置インデックス

Elasticsearchではドキュメントをindexする際、文字列を解析して転置インデックスを作成します。

転置インデックスとはRDBのインデックスと似て本の索引のような情報です。文字列に出現する単語を解析しそれがどのドキュメントに現れるのかをまとめます。

  1. Alice and Bob exchange keys.
  2. Charlie and Bob do some math.

例えば上記2つの文字列で転置インデックスを作ると

  • alice は1番のドキュメントに含まれる
  • bob は1, 2番のドキュメントに含まれる
  • charlie は2番のドキュメントに含まれる
  • key は1番のドキュメントに...

という形で、単語とそれが含まれるドキュメントIDを紐付けていきます。検索のときには検索キーワードと転置インデックスを突き合わせて、単語が存在するドキュメントIDを特定します。

アナライザ

上記例で単語が小文字になっていたり、keyskeyになっていましたが誤字ではなくElasticsearchの処理によるものです。 転置インデックス作成前に走る処理をアナライザ(analyzer)と言います。

アナライザは3つのコンポーネントで構成されています。

  1. Character Filter
  2. Tokenizer
  3. Token Filter

Tokenizerが単語を抽出し分かち書きするコンポーネントで、Character Filter, Token FilterはTokenizerの前後の処理です。 Elasticsearchでは標準でいくつか用意されていますが、用途に応じて独自に定義したりプラグインを導入することも可能です。

アナライザの動きはAnalize APIで確認することが出来ます。

Character Filter

分かち書きの前処理になります。

HTMLタグを除去したり、文字列を置換したりすることが出来ます。 たとえばHTMLタグを除去、 色色 という文字列を 色々 に変換。というCharacter Filterを設定して <b>人生色色</b> という文字列がどうなるか見てみます。

リクエスト

GET _analyze
{
  "char_filter": [
    "html_strip",
    {
      "type" : "mapping",
      "mappings" : [
        "色色 => 色々"
        ]
    }
    ],
    "text": "<b>人生色色</b>"
}

レスポンス

{
  "tokens" : [
    {
      "token" : "人生色々",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "word",
      "position" : 0
    }
  ]
}

HTMLタグと文字列の置換を行えました。 この処理により以降のTokenizerやToken Filterで単語を扱いやすくしたり転置インデックスの精度をあげることが出来ます。

Tokenizer

Character Filterにより整形された文字列を分かち書きするコンポーネントです。 Tokenizerは重要で様々な種類が提供されているので、利用ケースによって適切なものを選択してください。

https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenizers.html

今回はElasticsearchが用意しているTokenizerのStandard tokenizerを利用してみましょう。

The standard tokenizer provides grammar based tokenization (based on the Unicode Text Segmentation algorithm, as specified in Unicode Standard Annex #29) and works well for most languages.

文法ベースのtokenizerで大体の言語で利用できるようです。

リクエスト

GET _analyze
{
  "analyzer": "standard",
  "text": "tom&jerry are good friends."
}

レスポンス

{
  "tokens" : [
    {
      "token" : "tom",
      "start_offset" : 0,
      "end_offset" : 3,
      "type" : "<ALPHANUM>",
      "position" : 0
    },
    {
      "token" : "jerry",
      "start_offset" : 4,
      "end_offset" : 9,
      "type" : "<ALPHANUM>",
      "position" : 1
    },
    {
      "token" : "are",
      "start_offset" : 10,
      "end_offset" : 13,
      "type" : "<ALPHANUM>",
      "position" : 2
    },
    {
      "token" : "good",
      "start_offset" : 14,
      "end_offset" : 18,
      "type" : "<ALPHANUM>",
      "position" : 3
    },
    {
      "token" : "friends",
      "start_offset" : 19,
      "end_offset" : 26,
      "type" : "<ALPHANUM>",
      "position" : 4
    }
  ]
}

tom jerry are good friends とキレイに単語毎に別けることができました。 tom&jerry と入力してもきちんと分けてくれます。

日本語を扱う場合

日本語を分かち書きはそのままでは上手くいきません。

トムとジェリーはとても仲良しです

という短文をStandard analyzerで実行すると トム ジェリー と分かち書きされてしまいます。 Standard analyzerは英語であれば単語やスペース毎に区切れたりしますが、日本語の分かち書きには向かないようです。

日本語の場合には辞書等を用いて、意味のある単語で区切る形態素解析を行う必要があります。 Elasticsearchではそのためのプラグインkuromojiを提供しています。

https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html

sudo bin/elasticsearch-plugin install analysis-kuromoji

Elasticsearchをインストールしたディレクトリで上記コマンドを実行するだけで利用が可能です。

tokenizerkuromoji_tokenizer を指定して先ほどと同じ文字列を解析してみましょう。

リクエスト

GET _analyze
{
  "tokenizer": "kuromoji_tokenizer",
  "text": "トムとジェリーはとても仲良しです"
}

レスポンス

{
  "tokens" : [
    {
      "token" : "トム",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "と",
      "start_offset" : 2,
      "end_offset" : 3,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "ジェリー",
      "start_offset" : 3,
      "end_offset" : 7,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : "は",
      "start_offset" : 7,
      "end_offset" : 8,
      "type" : "word",
      "position" : 3
    },
    {
      "token" : "とても",
      "start_offset" : 8,
      "end_offset" : 11,
      "type" : "word",
      "position" : 4
    },
    {
      "token" : "仲良し",
      "start_offset" : 11,
      "end_offset" : 14,
      "type" : "word",
      "position" : 5
    },
    {
      "token" : "です",
      "start_offset" : 14,
      "end_offset" : 16,
      "type" : "word",
      "position" : 6
    }
  ]
}

トム ジェリー とても 仲良し です と日本語の単語単位で分かち書きをしてくれました。 kuromojiにはtokenizerだけでなく、Character Filter, Token Filterも用意されています。

リクエスト

GET _analyze
{
  "char_filter": ["kuromoji_iteration_mark"],
  "tokenizer": "kuromoji_tokenizer",
  "filter": [
    "kuromoji_baseform",
    "kuromoji_part_of_speech"
    ], 
  "text": "日々ネズミを捕まえています"
}

レスポンス

{
  "tokens" : [
    {
      "token" : "日日",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "ネズミ",
      "start_offset" : 2,
      "end_offset" : 5,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "捕まえる",
      "start_offset" : 6,
      "end_offset" : 9,
      "type" : "word",
      "position" : 3
    },
    {
      "token" : "いる",
      "start_offset" : 10,
      "end_offset" : 11,
      "type" : "word",
      "position" : 5
    }
  ]
}
  • 日々 の繰り返し文字を 日日 と変換してくれる Character Filter kuromoji_iteration_mark
  • 捕まえて捕まえる にまるめてくれる kuromoji_baseform
  • 等の品詞を除外してくれる kuromoji_part_of_speech

用途に合わせて様々な設定をすることが出来ます。

GET _analyze
{
  "analyzer": "kuromoji", 
  "text": "日々ネズミを捕まえています"
}

analyzerkuromoji を設定することで細かく指定しないで使うことも可能です。

kuromoji公式

kuromoji analyzer | elastic

Token Filter

形態素解析して分かち書きした単語への後処理になります。

.filter に単語を小文字化する lowercase、 品詞を除外する stop を指定してみます。

リクエスト

GET _analyze
{
 "tokenizer": "standard",
 "filter": ["lowercase", "stop" ],
 "text": "To Be Or Not To Be, That Is The Question"
}

レスポンス

{
  "tokens" : [
    {
      "token" : "question",
      "start_offset" : 32,
      "end_offset" : 40,
      "type" : "<ALPHANUM>",
      "position" : 9
    }
  ]
}

to be or not 等の品詞は除外され、最終的には小文字化された question のみが残りました。 .filter の順序を stop lowercase の順にした場合、To Be 等の大文字では品詞が除外されないため結果が異なるので、実行順序に気をつけましょう。

Token Filterの種類については公式をご参考ください。

アナライザのまとめ

Character Filter → Tokenizer → Token Filter と処理を通した単語とドキュメントIDを元に転置インデックスが作成されます。

アナライザを通すことにより、不要な単語を覗いたり語句をまるめたりすることで精度の高い全文検索が可能になります。

検索クエリ

アナライザはドキュメントをindexする時だけでなく、検索APIを呼んだ時の検索キーワードに対しても実行されます。

Elasticsearch入門 転置インデックス の検索した際には、アナライザによって elasticsearch 入門 転置インデックス の形に言語処理され、転置インデックスと突き合わせを行います。

マッピング

その1では、Elasticsearchのマッピングについて少しだけ言及していました。

各フィールドには型を定義することができ、それをmappingと言います。Elasticsearchではマッピングが未指定の場合、documentの値から自動で型を定義してくれます。

マッピングはドキュメントのスキーマ定義であり、フィールド名や型、どのアナライザを利用するか等を保持するメタ情報です。

Elasticsearchでマッピング出来る型は、公式をご参考ください。

Field data type | elastic

主要なのとしては以下があります。

  • boolean
    • true false の真偽値
  • long, integer, short, byte, double, float
    • 数値
  • object
    • JSONオブジェクト
  • ip
    • IPアドレス(IPv4, IPv6)
  • geo_point, geo_shape
    • 地理情報
  • text
    • 部分一致に利用する文字列
    • アナライザを通す
  • keyword
    • 完全一致に利用する文字列
    • アナライザを通さない
    • メールアドレス、URL、タグ等で利用

定義リクエスト

ブログ情報を格納するインデックス blogs を定義してみましょう。

リクエスト

PUT blogs
{
  "mappings": {
    "properties": {
      "content" : {
        "type": "text",
        "analyzer": "kuromoji"
      },
      "url" : {
        "type": "keyword"
      },
      "timestamp": {
        "type": "date"
      },
      "author" : {
        "type": "text",
        "fields": {
          "raw" : {
            "type" : "keyword"
          }
        }
      }
    }
  }
}

このリクエストによって blogs インデックスにマッピングを作成しました。

  • content フィールドはtext型で、日本語の記事内容が入る前提なのでアナライザにkuromojiを指定
  • url フィールドは完全一致でしか利用しないため、型はkeyword
  • timestamp フィールドにはタイムスタンプを入力する想定でdate型

author フィールドは著者名で検索する場合を想定しtext型にしたのですが、完全一致を利用するケースも想定されるため author.raw をkeyword型にしました。 このように1つのフィールドで複数の使い方が想定される場合、マルチフィールドを定義することができます。

multi-fields | elastic

マッピングを定義したのでドキュメントをindexしてみましょう。

リクエスト

PUT blogs/_bulk
{"index" : {"_id" : 1}}
{"content" : "Elasticsearchやってみた", "url" : "http://test.example.com", "author" : "中村",  "timestamp" : 111}
{"index" : {"_id" : 2}}
{"content" : "Elasticsearchのアナライザやマッピングを勉強してみた", "url" : "http://test1.example.com", "author" : "NKMR",  "timestamp" : 2}
{"index" : {"_id" : 3}}
{"content" : "池袋のサウナに行ってみたレポ", "url" : "https://sauna.example.com", "author" : "saunakamura",  "timestamp" : 3, "is_sauna" : true}

indexが成功したので、今度はマッピングを取得してみます。

リクエスト

GET blogs/_mapping

レスポンス

{
  "blogs" : {
    "mappings" : {
      "properties" : {
        "author" : {
          "type" : "text",
          "fields" : {
            "raw" : {
              "type" : "keyword"
            }
          }
        },
        "content" : {
          "type" : "text",
          "analyzer" : "kuromoji"
        },
        "is_sauna" : {
          "type" : "boolean"
        },
        "timestamp" : {
          "type" : "date"
        },
        "url" : {
          "type" : "keyword"
        }
      }
    }
  }
}

自分で定義したマッピング情報と、新しく追加したフィールド情報が返ってきました。

日本語で検索して無事にHITしました。

Elasticsearchでは、既存のマッピングを後から変更することが難しいです。 なので、利用用途をしっかり洗い出し定義しましょう。

最後に

今回はマッピングやアナライザ等、Elasticsearchのメタ情報や仕組みを勉強しました。

次回はクラスタやノードについて書いていこうと思います。