Quantcast
Channel: mattintosh note
Viewing all articles
Browse latest Browse all 891

Elasticsearch とオブジェクト指向。Object datatype と Nested datatype の違い

$
0
0

Elasticsearch のデータタイプには Object datatype と Nested datatype というものがある。

Object datatype | Elasticsearch Reference [6.6] | Elastic

Nested datatype | Elasticsearch Reference [6.6] | Elastic

これを説明する前にオブジェクト指向な考え方について知っておかなければいけないが、これを書いている人間はつい最近まで「オブジェクト指向とはなんぞや?」という人間だったため詳しい人からすると誤った考え方をしているかもしれないのでその辺をご容赦いただいた上で読んでもらいたい。

オブジェクト指向的なデータの作り方

オブジェクト指向的な考え方の場合、データを下記のような構造で作成すると思う。こういうデータ構造が出来ないわけではないし間違いでもない。

{"first_name": "John",
  "last_name": "Smith"
}

ではオブジェクト指向的な考えでデータを入れるならどういう構造になるだろうか?first_namelast_nameも名前を構成する部品なので「名前」にぶら下げるのがいいのだろう。

{"name": {"first": "John",
    "last": "Smith"
  }}

「え、なんか面倒臭くないですか!?」なんて思ったりするかもしれない。この JSONを見るとネストされて若干複雑になっていたりキーも増えているし、デメリットしか無さそうに思える。(そう思っていた時期が私にはありました)

しかし、フィールドへのアクセスはフラットな場合とほとんど変わらない。

name.first
name.last

データの見方を変えてみる

JSONのままではわかりづらいのでそれぞれを表にしてみよう。

最初に書いた非オブジェクト指向的な考え方の場合、フィールドはフラットな関係なのでこんな感じになる。よくある表だ。

オブジェクト指向的な考え方
userfirst_name (string)last_name (string)
John SmithJohnSmith
Alice WhiteAliceWhite

ではオブジェクト指向的な考えで作成した JSONを表にしてみよう。

オブジェクト指向的な考え方①
username (object)
first (string)last (string)
John SmithJohnSmith
Alice WhiteAliceWhite

あるいはこういうイメージの仕方かもしれない。

オブジェクト指向的な考え方②
username (object)
John Smithfirst (string)last (string)
JohnSmith
Alice Whitefirst (string)last (string)
AliceWhite

Excelとかを使う人であればセルの結合はよく使っていると思うのですぐイメージ出来ると思う。非オブジェクト指向的な考え方であってもデータベースでよく見る構造だし間違っちゃいない。でも、オブジェクト指向的な考え方をすることによってそれぞれのフィールドに関連性をもたせることが出来るようになる。

さらに「オブジェクト」というものを意識した場合はこんな感じになるだろうか。「name」フィールドには直接「first」や「last」のデータが入っているわけではなく、「name Object」というものが入っていて、その中に「first」と「last」というフィールドが用意されているのだ。

オブジェクト指向的な構造
username (object)
John Smith
"name" Object
first (string)last (string)
JohnSmith
Alice White
"name" Object
first (string)last (string)
AliceWhite

オブジェクト指向的な構造の場合、

「John Smith さんの『姓』のデータと『名』のデータをちょうだい」

と、2つのお願いをしなくてはいけない。また、我々人間には『姓』と『名』から『名前』という関連性をイメージできるが、機械からすれば『姓』と『名』の関連性はわからないだろう。

オブジェクト指向的な構造であれば

「John Smith さんの『名前』のデータちょうだい」

と、お願いすればあとは自分で好きに出来るし、機械からしても『姓』と『名』がどういうものかはわからないが『名前』というものに紐付いた何かなんだろう、くらいには感じるんじゃなかろうか。

Elasticsearch にネストしたデータを入れてみる

Elasticsearch にネストされたデータを入れた場合、特にマッピングの設定をしていなくても Dynamic templates がよしなにやってくれる。

f:id:mattintosh4:20190226162254p:plain
Kibana の Dev Tools でサンプルデータを投入してみる

PUT my_index/_doc/1
{"name": {"first": "John",
    "last": "Smith"
  }}PUT my_index/_doc/2
{"name": {"first": "Alice",
    "last": "White"
  }}GET my_index/_search

マッピングはこんな感じになる。通常、フィールド名のすぐ下には typeの指定が来るが、ネストしている場合は ❶ の部分に propertiesが来る。これが Elasticsearch で言うところの Object datatypeである。型指定は下層の値を格納する ❷ の部分で設定する。

GET my_index/_mapping
{"my_index" : {"mappings" : {"_doc" : {"properties" : {"name" : {"properties" : {❶
              "first" : {❷
                "type" : "text",
                "fields" : {"keyword" : {"type" : "keyword",
                    "ignore_above" : 256}}},
              "last" : {❷
                "type" : "text",
                "fields" : {"keyword" : {"type" : "keyword",
                    "ignore_above" : 256}}}}}}}}}}

Luceneで検索する場合は親と子のフィールドを .で連結する。

name.first:John AND name.last:Smith

Object datatype と Nested datatype について知る

特にマッピングをせずにデータを投入した場合、ネストされたデータは Object datatypeになる。検索も普通に効くし、特に問題が無いように思われるが、実は投入した _sourceの構造と Elasticsearch の内部での構造が異なってしまうことがある。

例えば、以下のように studentsフィールドを配列にして複数のデータを投入したとする。

PUT my_index/_doc/1
{"students": [{"first": "John",
      "last": "Smith"
    },
    {"first": "Alice",
      "last": "White"
    }]}

これ、入れたとおりの構造になっているかと思いきや、Elasticsearch の内部ではこういう風に解釈されているのである。

{"students": {"first": ["John", "Alice"],
    "last": ["Smith", "White"]}}

students.first:Johnstudents.last:Smithで検索すればちゃんとマッチするし、特に問題ないのでは?と思うが、誤った結果を招くこともある。

例えば、本来は存在しない「John White」という人を検索してみるとする。

"John White"を検索するクエリ

GET my_index/_search
{"query": {"query_string": {"query": "students.first:John AND students.last:White"
    }}}

「John White」という人はいないので結果は0件であることが期待されるが、Elasticsearch からすれば、students.first:Johntrueを返すし、students.last:Whitetrueを返すので先程投入したデータが返ってくる。

{"took" : 17,
  "timed_out" : false,
  "_shards" : {"total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0},
  "hits" : {"total" : 1,
    "max_score" : 0.5753642,
    "hits" : [{"_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.5753642,
        "_source" : {"students" : [{"first" : "John",
              "last" : "Smith"
            },
            {"first" : "Alice",
              "last" : "White"
            }]}}]}}

このような結果にならないように、データ構造を維持しておけるデータタイプが Nested datatypeである。ネストされたオブジェクトを Nested datatype として扱いたい場合はマッピングnestedを指定する。

PUT my_index/
{"mappings": {"_doc": {"properties": {"students": {"type": "nested"
        }}}}}

「データ構造を維持したまま保存できる Nested datatype の方が Object datatype よりもよいのではないか?」と思うが、Nested datatype では Luceneなどからネストされたフィールドに対して検索が出来ないという制限があるため、student.first:Johnといった感じの気軽な検索が出来なくなる。

Nested datatype のフィールドの検索には Nested Query という方法が用意されていて、使い方としてはネストの親を pathで指定し、そこを基点に検索するという感じ。

では、nested されたフィールドに検索をかけて誤った検索結果が返ってこないか試してみよう。

"Nested Query"のサンプル

# JohnSmithの検索
GET my_index/_search
{"query": {"nested": {"path": "students",
      "query": {"query_string": {"query": "students.first:John AND students.last:Smith"
        }}}}}

# JohnWhiteの検索(存在しないので何も出ない)
GET my_index/_search
{"query": {"nested": {"path": "students",
      "query": {"query_string": {"query": "students.first:John AND students.last:White"
        }}}}}

きっと期待通りの結果が得られると思う。

Kibana での見え方

Kibana では「配列内のオブジェクトはうまくサポートしない」と表示されるが、この表示は Object datatype も Nested datatype も同じ。

f:id:mattintosh4:20190226205542p:plain
Kibana - 配列内のオブジェクトの扱い

Index Patterns を見ても下層のフィールドはきちんと登録されるので nestedになっているかどうかはマッピング情報を見ないとわからない。

f:id:mattintosh4:20190226233544p:plain
Kibana - Index Patterns

Nested datatype のメリットを活かしつつ Luceneも使いたい

Nested datatype で Luceneが使えないということは Kibana の検索バーなどからも検索が出来なくなってしまうということであり、これは結構痛い。(これは Grafana も同様だったが、こちらは issue に要望が上がっていたので近々サポートされるかもしれない)

対応策として、copy_toでトップレベルの任意のフィールドに値をコピーしておくという方法がある。

下記は students.firstfirst_namesに、students.lastlast_namesにコピーするマッピングの例。

PUT my_index/
{"mappings": {"_doc": {"properties": {"students": {"type": "nested",
          "properties": {"first": {"type": "text",
              "copy_to": "first_names", ❶
              "fields": {"keyword": {"type": "keyword",
                  "ignore_above": 256}}},
            "last": {"type": "text",
              "copy_to": "last_names", ❷
              "fields": {"keyword": {"type": "keyword",
                  "ignore_above": 256}}}}}}}}}

ツリーで書くとこんな感じだろうか。

_doc
|
+-- students
|   |
|   +-- [0]
|   |   |
|   |   +-- first -> copy_to: first_names ❶
|   |   |
|   |   +-- last  -> copy_to: last_names ❷
|   |
|   +-- [1]
|       |
|       +-- first -> copy_to: first_names ❶
|       |
|       +-- last  -> copy_to: last_names ❷
|
+-- first_names ❶
|
+-- last_names ❷

名前のデータを入れてからマッピングを見てみると _sourceには存在しない first_nameslast_namesのフィールドが増えているのがわかる。

{"my_index" : {"mappings" : {"_doc" : {"properties" : {"first_names" : {"type" : "text",
            "fields" : {"keyword" : {"type" : "keyword",
                "ignore_above" : 256}}},
          "last_names" : {"type" : "text",
            "fields" : {"keyword" : {"type" : "keyword",
                "ignore_above" : 256}}},
          "students" : {"type" : "nested",
            "properties" : {"first" : {"type" : "text",
                "fields" : {"keyword" : {"type" : "keyword",
                    "ignore_above" : 256}},
                "copy_to" : ["first_names"
                ]},
              "last" : {"type" : "text",
                "fields" : {"keyword" : {"type" : "keyword",
                    "ignore_above" : 256}},
                "copy_to" : ["last_names"
                ]}}}}}}}}

表にするとこんな感じだろうか。

my_index/_doc
students (object array)first_names (array)last_names (array)
Object[0]
first (string)last (string)
JohnSmith
Object[1]
first (string)last (string)
AliceWhite
  • John
    -> /students[0]/first
  • Alice
    -> /students[1]/first
  • Smith
    -> /students[0]/last
  • White
    -> /students[1]/last

Kibana で検索する場合は copy_toで作成したフィールドに対して検索をかければよい。

first_names:John

余談だが、copy_toはコピー先を複数指定することができるので、例えば姓と名の両方を集約したフィールドを作成することもできる。下記のように「❶ 名前検索」「❷ 名字検索」、用のフィールドに加えて「❸ 氏名検索」用のフィールドへコピーしてあげればよい。

        :
        "students": {"type": "nested",
          "properties": {"first": {"type": "text",
              "copy_to": ["first_names", ❶
                "full_names" ❸
              ],
              "fields": {"keyword": {"type": "keyword",
                  "ignore_above": 256}}},
            "last": {"type": "text",
              "copy_to": ["last_names", ❷
                "full_names" ❸
              ],
              "fields": {"keyword": {"type": "keyword",
                  "ignore_above": 256}}}}}
        :

メモ:copy_to のデータの取り出し方

copy_toで作成したフィールドは _sourceに含まれないので script_fieldsまたは docvalue_fieldsで取り出す必要がある。

この2つはデフォルトの _sourceの返し方が異なるので必要に応じて設定しておく。また、取り出したフィールドは _source内の配列順とは限らないので注意。

リクエスト方法_source
scripted_fieldsfalse
docvalue_fieldstrue

scripted_field を使って copy_to のコピー先のフィールドを出力するクエリ

GET my_index/_search
{"_source": true,
  "script_fields": {"任意のフィールド名": {"script": {"source": "doc['first_names.keyword'].values"
      }}}}
{"took" : 38,
  "timed_out" : false,
  "_shards" : {"total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0},
  "hits" : {"total" : 1,
    "max_score" : 1.0,
    "hits" : [{"_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {"students" : [{"first" : "John",
              "last" : "Smith"
            },
            {"first" : "Alice",
              "last" : "White"
            }]},
        "fields" : {"任意のフィールド名" : ["Alice",
            "John"
          ]}}]}}

docvalue_fieldsでは fieldをそのまま指定すればいい。formatには use_field_mappingを指定しておけばマッピングを元に決めてくれる。

docvalue_fields を使って copy_to のコピー先のフィールドを出力するクエリ

GET my_index/_search
{"_source": false, 
  "docvalue_fields": [{"field": "first_names.keyword",
      "format": "use_field_mapping"
    },
    {"field": "last_names.keyword",
      "format": "use_field_mapping"
    }]}
{"took" : 13,
  "timed_out" : false,
  "_shards" : {"total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0},
  "hits" : {"total" : 1,
    "max_score" : 1.0,
    "hits" : [{"_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "fields" : {"first_names.keyword" : ["Alice",
            "John"
          ],
          "last_names.keyword" : ["Smith",
            "White"
          ]}}]}}

それでは良い Elasticsearch ライフを😊


Viewing all articles
Browse latest Browse all 891

Trending Articles