Elasticsearch Nested Type vs Array Objects

Kunihiko Kido
Hello! Elasticsearch.
8 min readFeb 23, 2015

--

Nested Type と Array Objects の違いと使い方

Elasticsearchには、辞書形式のデータの配列(複数のプロパティを持つオブジェクトの配列)をインデックスする際に、Nested と言うフィールドタイプが用意されています。

インデックスする際にJSONデータの内容は同じでもNested型でマッピング定義されているのか?単にオブジェクトの配列としてマッピング定義されているのかで、検索やソートなどで動作が異なるので注意が必要です。と言うか検索要件によって使い分ける必要が有ります。

デフォルトのダイナミックテンプレートでは、単純にオブジェクトの配列としてマッピング定義されます。

Array Objects

最初に、通常のオブジェクトの配列についての説明です。

BtoB向けのECサイトを例に、商品は顧客ごとに販売価格が異なる仕様だとします。

{
"product_id": "A000001",
"prices": [
{
"customer_id": "001",
"price": 1980
},
{
"customer_id": "002",
"price": 2000
},
{
"customer_id": "003",
"price": 1900
}
]
}

上記のデータは、単純なオブジェクトの配列でマッピングされている場合、以下のデータと同じような意味合いでインデックスされます。

{
"product_id": "A000001",
"prices.customer_id": ["001", "002", "003"],
"prices.price": [1980, 2000, 1900]
}

そのため、customer_idとpriceの組み合わせは、インデックス時に維持されないので、「customer_id ==001 AND price <= 1980」と言う検索条件だけでなく、「customer_id == 001 AND price <= 1900」と言う検索条件でもマッチするようになります。

{
"query": {
"bool": {
"must": [
{"term": {"prices.customer_id": "001"}},
{"range": {"prices.price": "lte": 1980}}
]
}
}
}

※ このクエリもマッチするし、

{
"query": {
"bool": {
"must": [
{"term": {"prices.customer_id": "001"}},
{"range": {"prices.price": "lte": 1900}}
]
}
}
}

※ このクエリもマッチする。

Nested Type

顧客ごとの販売価格の組み合わせを維持したインデックスを作成した場合は、Nested Typeで定義します。

インデックスする元のJSONデータは同じですが、pricesのマッピング定義の型を nested で定義します。

{
"type1": {
”properties”: {
"prices":{
"type": "nested",
"properties": {
"customer_id": {"type": "string"},
"price": {"type": "long"}
}
}
}
}
}

nested で定義されたpricesは元のデータ構造の意味合いと同じように、顧客と販売価格の組み合わせが維持されてインデックスされます。

{
"customer_id": "001",
"price": 1980
}
{
"customer_id": "002",
"price": 2000
}
{
"customer_id": "003",
"price": 1900
}
{
"product_id": "A000001"
}

さらに、検索する際は次の例のように、元のクエリを少し変更して検索します。

{
"query": {
"nested": {
"path": "prices",
"bool": {
"must": [
{"term": {"prices.customer_id": "001"}},
{"range": {"prices.price": "lte": 1980}}
]
}
}
}
}

nestedに対応する前は、「customer_id == 001 AND price <= 1900」という条件もマッチしていましたが、意図したようにマッチしなくなります。

{
"query": {
"nested": {
"path": "prices",
"bool": {
"must": [
{"term": {"prices.customer_id": "001"}},
{"range": {"prices.price": "lte": 1900}}
]
}
}
}
}

※ このクエリはマッチしなくなる。

Sorting by nested fields

nestedで定義されたデータは、ソートするときも対象のオブジェクトを指定するフィルターが使用できるので、例えば、顧客IDが001の販売価格を対象にソートすると言った正確なソートもできます。

{
"sort": [
{
"prices": {
"order": "asc",
"mode": "min",
"nested_filter": {
"term": {"prices.customer_id": "001"}
}
}
}
]
}

※ 上記の例では、顧客IDが001の最小価格で昇順ソートする場合のクエリ例です。上記データ例商品IDがA00001のドキュメントでは、1980がソートの値として使用されます。

Nested Aggregation

nestedで定義されたデータは、Aggregationでも使用することができます。例えば顧客IDごとの最小販売価格を集計するなど正確な集計が可能です。

{
"aggs": {
"prices": {
"nested": {
"path": "prices"
},
"aggs": {
"group_by_customer": {
"terms": {
"field": "prices.customer_id"
},
"aggs": {
"min_price": {
"min": {"field": "prices.price"}
}
}
}
}
}
}
}

※ nestedで定義されている場合は、顧客ID001に対して販売価格1980、002に対して2000、003に対して1900と集計されますが、通常のオブジェクトの配列でインデックスされている場合は、顧客ID001、002、003の集計結果はすべて1900になります。

その他の用途では、例えば、親子関連の階層構造を持っている商品カテゴリが1商品に対して複数設定可能な使用の場合、そのカテゴリの親子関連を維持した集計結果を取得したいという要望があるかと思いますが、この場合もNested Aggregationを使用すると比較的簡単に実現することができます。

NestedもObjectも両方使う

例えば、検索のフィルタリングではNestedを使って、集計は通常のオブジェクトの配列で行うなど、両方を使い分けたい場合はどうするか?

その場合は、先ほどのnestedのマッピング定義で、include_in_parent オプションを true に設定します。

{
"type1": {
”properties”: {
"prices":{
"type": "nested",
"include_in_parent": true,
"properties": {
"customer_id": {"type": "string"},
"price": {"type": "long"}
}
}
}
}
}

こうすることで、nestedまたは、array objectsの検索や集計を利用シーンに合わせて、使い分けることができます。

まとめ

なんとなくですが、インデックスする元のJSONデータを検索パフォーマンスを重視して、わざわざフラットなデータ設計しなくても、デフォルトではフラットな構造になってインデックスされるので、検索要件によってデータ設計を変更しなくても、データ構造は変えずに、マッピング定義でNestedに対応するのかどうか決定すればよい仕組みになっているのかな?と思いました。

--

--