Elasticsearch のマッピングについて調べてみた!

ドキュメントの保存と検索はざっくりとわかったので、マッピングについて調べました。

マッピングとは

ドキュメントがどのように保存され、どのようにインデックスが作成されるかを定義するのがマッピングらしいです。RDBで言うスキーマかなと最初思ったのですが、だいぶ違うものみたいなので同じようなものとは考えないほうがいいと思います。

公式サイトのドキュメントでは以下のように説明がありました。

Mapping is the process of defining how a document, and the fields it contains, are stored and indexed.

Mapping | elastic docs

マッピングは以下の2つの定義を持っています。

  • Meta-fields - ドキュメントに関連付けられたメタデータをどのように扱うかを設定するために使用
  • Fields or properties - ドキュメントに関連するフィールドか、プロパティのリスト

定義だけだとわかりにくいので、実際に触って見る必要がありそうです。

マッピングを静的に設定

マッピングは動的または静的に設定できます。ここでは静的に設定してみます。

マッピングの設定は REST API で行うことができるようです。インデックスを作成すると同時にマッピングを指定します。

PUT /employee
{
  "mappings": {
    "properties": {
      "name": { "type": "text" },
      "age": { "type": "integer" },
      "belog": { "type": "keyword" }
    }
  }
}

プロパティとしてドキュメントが持つフィールドとその格納方法を指定しました。

どのようなマッピングになっているかは以下のリクエストで確認できます。

GET /employee/_mapping
{
  "employee" : {
    "mappings" : {
      "properties" : {
        "age" : {
          "type" : "integer"
        },
        "belog" : {
          "type" : "keyword"
        },
        "name" : {
          "type" : "text"
        }
      }
    }
  }
}

マッピングが正しく設定されているかを検証してみました。 textkeyword フィールドはそれぞれデータの持ち方が異なります。 text全文検索のために Elasticsearch が文字列を解析しますが、 keyword の場合は文字列全体を一つの値として扱うみたいです。ざっくりと説明すると、 text の場合は文字列が要素(動詞とか、形容詞とか)に分割されて、keyword はされないといった感じでしょうか。

text フィールドに対して全文検索すると、文字の一部分が一致するだけでヒットするようです(一部分という表現が曖昧なのはよくわかっていないからです。このあたりは後で調査)。全文検索には match 句を使います。

POST /employee/_doc
{
  "name": "BOOK STORE",
  "age": 27,
  "belong": "normal deveoper"
}
GET /employee/_search
{
  "query": {
    "match": {
      "name": "BOOK"
    }
  }
}
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.2876821,
    "hits" : [
      {
        "_index" : "employee",
        "_type" : "_doc",
        "_id" : "_tZGCnMBWU8rPLHxmgBS",
        "_score" : 0.2876821,
        "_source" : {
          "name" : "BOOK STORE",
          "age" : 27,
          "belong" : "normal developper"
        }
      }
    ]
  }
}

keyword フィールドは文字列が全部一致しないと検索結果に表示されません。 keyword フィールドに対する文字列の検索では term 句を使用します。検索結果は長いので省略します。

GET /employee/_search
{
  "query": {
    "term": {
      "belong": {
        "value": "normal"
      }
    }
  }
}

文字列全部が一致していれば、ヒットします。

GET /employee/_search
{
  "query": {
    "term": {
      "belong": {
        "value": "normal developper"
      }
    }
  }
}

ちなみに、フィールドのデータ・タイプは以下に網羅されています。詳細な定義や説明も載ってます。

複数フィールドの設定

Elasticsearch では一つのフィールドに対して、データ・タイプを複数設定することが可能です。例えば textkeyword を同時に定義し、検索するときにどちらのデータ・タイプとして使用するか、指定します。複数のデータ・タイプを持てることによって、検索がしやすくできるということでしょうね。

というわけで、上記でやってみたマッピング定義を再び使い、複数フィールドの設定をしてみます。

一度インデックスを削除。

DELETE /employee

再び、インデックスを作成すると同時にマッピングを定義します。

PUT /employee
{
  "mappings": {
    "properties": {
      "name": { "type": "text" },
      "age": { "type": "integer" },
      "belong": { 
        "type": "text",
        "fields": {
          "raw": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      }
    }
  }
}

上記では belong フィールドに複数のデータ・タイプを定義しました。 fields という項目がありますが、この項目を指定することで複数のデータ・タイプを定義できます。 fields には2つ目のデータ・タイプと、2つ目の名前を定義します。上記では raw という名前で keyword データ・タイプを定義しました。 ignore_above という項目は keyword データ・タイプを使用するときに追加で指定できる項目で、文字列の長さが指定数以上のときにインデックスを作らない設定ができます(おそらく負荷の関係?)。

検索はこうなります。

belongtext として使う場合。

GET /employee/_search
{
  "query": {
    "match": {
      "belong": "normal"
    }
  }
}

belongkeyword として使う場合。

GET /employee/_search
{
  "query": {
    "term": {
      "belong.raw": {
        "value": "normal developper"
      }
    }
  }
}

メタフィールド

マッピングではメタフィールドの振る舞いを設定できるみたいです。メタフィールドは全て _ で始まる規則があるようで、検索結果の値に含まれています。また、検索にも使用することができます。

メタフィールドは以下のページに一覧と詳しい解説があります。

検索結果に含まれているメタフィールドは例えばこんな感じかなと。

{
  (省略)
    "hits" : [
      {
        "_index" : "employee",
        "_type" : "_doc",
        "_id" : "ifZgDXMBoJXfK5lP7faD",
        "_score" : 1.0,
        "_source" : {
          "name" : "BOOK STORE",
          "age" : 27,
          "belong" : "normal developper"
        }
      }
    ]
  }
}

ドキュメントの一意のIDを表す _id メタフィールドの値を使用した検索はこうなります。IDの指定が配列になっているのは複数のIDを検索できるからですね。

GET employee/_search
{
  "query": {
    "terms": {
      "_id": ["ifZgDXMBoJXfK5lP7faD"]
    }
  }
}

_id メタフィールドのID値は Get API でも使用されます。

GET employee/_doc/ifZgDXMBoJXfK5lP7faD

_source メタフィールドはドキュメント本体を示すメタフィールドになります。マッピングの定義で _source に含める、含めないフィールドを設定することができるようです。あくまでも _source に含まれるかを設定できるようなので、含まれていない値を使った検索もできます。

// age フィールドを除外
PUT employee
{
  "mappings": {
    "_source": {
      "excludes": [
          "age"
        ]
    }
  }
}

// ドキュメントを追加。除外されるフィールドがあってもいい。
POST /employee/_doc
{
  "name": "BOOK STORE",
  "age": 27,
  "belong": "normal developper"
}

// 除外されているフィールドを使って検索
GET employee/_search
{
  "query": {
    "term": {
      "age": "27"
    }
  }
}

// 検索結果に age は含まれない
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "employee",
        "_type" : "_doc",
        "_id" : "1BnyDXMBkS7Mh8swfSKP",
        "_score" : 1.0,
        "_source" : {
          "belong" : "normal developper",
          "name" : "BOOK STORE"
        }
      }
    ]
  }
}