욱'S 노트

Elasticsearch - Modeling Your Data:Nested Objects 본문

Programming/Elasticsearch

Elasticsearch - Modeling Your Data:Nested Objects

devsun 2016. 1. 18. 15:54

엘라스틱서치에서 하나의 도큐먼트에 대한 생성, 삭제, 업데이트는 atomic하다. 이 의미는 같은 도큐먼트내에 저장된 엔티티에도 동일하다. 예를 들어 우리는 블로그포스트와 그의 코멘트를 하나의 도큐먼트로 저장할 수 있다. 


PUT /my_index/blogpost/1

{

  "title": "Nest eggs",

  "body":  "Making your money work...",

  "tags":  [ "cash", "shares" ],

  "comments": [ 

    {

      "name":    "John Smith",

      "comment": "Great article",

      "age":     28,

      "stars":   4,

      "date":    "2014-09-01"

    },

    {

      "name":    "Alice White",

      "comment": "More like this please",

      "age":     31,

      "stars":   5,

      "date":    "2014-10-22"

    }

  ]

}


만약 다이나믹 매핑을 이용한다면 코멘트 필드는 오브젝트 필드로서 자동생성될 수 있다.


모든 내용이 같은 도큐먼트에 있기 떄문에 쿼리타임에 블로그 포스트와 코멘트를 조인할 필요가 없다. 그러므로 성능이 향상될 것이다.


문제는 도큐먼트가 다음과 같은 쿼리에 매치될 수 있다.


GET /_search

{

  "query": {

    "bool": {

      "must": [

        { "match": { "name": "Alice" }},

        { "match": { "age":  28      }} 

      ]

    }

  }

}



Alice는 31 이다 28이 아니다.


이유는 cross-object 매칭 때문인데 내부 오프젝트의 배열을 논할때 아래와 같은 단순한 키-값 형식으로 인덱싱을 하기 때문이다.


{

  "title":            [ eggs, nest ],

  "body":             [ making, money, work, your ],

  "tags":             [ cash, shares ],

  "comments.name":    [ alice, john, smith, white ],

  "comments.comment": [ article, great, like, more, please, this ],

  "comments.age":     [ 28, 31 ],

  "comments.stars":   [ 4, 5 ],

  "comments.date":    [ 2014-09-01, 2014-10-22 ]

}


Alice와 31, John과 2014-09-01의 일치는 잃어버리기 때문이다. 타입 오브젝트의 필드는 검색의 관점에서 하나의 오브젝트로 저장되는게 유용하기 때문이다. 


이러한 문제를 풀기 위해 nested objects가 디자인되었다. 각 내포된 오브젝트는 보이지않는 분리된 도큐먼트로 인덱스되는데 다음과 같다.


{ (1)

  "comments.name":    [ john, smith ],

  "comments.comment": [ article, great ],

  "comments.age":     [ 28 ],

  "comments.stars":   [ 4 ],

  "comments.date":    [ 2014-09-01 ]

}

{ (2)

  "comments.name":    [ alice, white ],

  "comments.comment": [ like, more, please, this ],

  "comments.age":     [ 31 ],

  "comments.stars":   [ 5 ],

  "comments.date":    [ 2014-10-22 ]

}

{ (3)

  "title":            [ eggs, nest ],

  "body":             [ making, money, work, your ],

  "tags":             [ cash, shares ]

}


(1) First nested object

(2) Second nested object

(3) The root or parent document


각각의 내포된 오브젝트들은 분리하여 인덱싱됨으로 오브젝트내에 필드들은 그들의 관계를 유지하게 된다. 우리는 같은 내포된 오브젝트내에서 매치가 발생하는지 쿼리를 수행할 수 있다.


이 뿐만 아니라 내포된 오브젝트들은 인덱싱되었으므로, 쿼리 타임에 빠르게 루트 도큐먼트와 조인할 수 있다.


이러한 추가적인 내포된 오브젝트들은 숨겨져있다. 이는 직접 접근할 수 없다는 것이다. 업데이트, 추가 삭제를 하기 위해서는 전체 도큐먼트를 다시 인덱스해야 한다. 이는 또한 중요한 점이 내포된 오브젝트 만으로 검색 결과로 리턴될 수 없다.


Nested Objects Mapping


내포된 필드를 세팅하는 것은 단순하다. 


PUT /my_index

{

  "mappings": {

    "blogpost": {

      "properties": {

        "comments": {

          "type": "nested", 

          "properties": {

            "name":    { "type": "string"  },

            "comment": { "type": "string"  },

            "age":     { "type": "short"   },

            "stars":   { "type": "short"   },

            "date":    { "type": "date"    }

          }

        }

      }

    }

  }

}


내포된 필드는 타입 오브젝트의 필드로서 같은 파라미터를 받아들인다.


이것으로 충분하다. 이제 어떤 코멘트 오브젝트도 분리된 내포 오브젝트로 인덱싱된다.


Querying a Nested Object


내포된 오브젝트들은 분리된 히든 도큐먼트로 인덱싱되기 때문에 그들에 직접 쿼리르 할 수 없다. 대신에 그들을 접근하기 위해서는 nested 쿼리를 이용해야 한다.


GET /my_index/blogpost/_search

{

  "query": {

    "bool": {

      "must": [

        { "match": { "title": "eggs" }}, (1)

        {

          "nested": {

            "path": "comments", (2)

            "query": {

              "bool": {

                "must": [ 

                  { "match": { "comments.name": "john" }},(3)

                  { "match": { "comments.age":  28     }}

                ]

        }}}}

      ]

}}}



(1) 타이틀 절은 루트 도큐먼트에 대해 수행된다.

(2) nested 절은 내포된 코멘트 필드로 스텝 다운된다. 더이상 루트 도큐먼트에 접근하지 않고 어떠한 내포된 도큐먼트의 필드에 접근한다. .

(3) comments.name와 comments.age 절은 동일한 내포 도큐먼트에 수행된다. 


Tip

내포 필드는 다른 내포 필드를 포함할 수 있다. 비슷하게 내포 쿼리는 내포 쿼리를 포함할 수 있다.


물론 내포 쿼리는 몇몇의 내포 도큐먼트와 매치될 수 있다. 각 매칭된 내포 도큐먼트는 자신의 연관된 스코어를 가지나 이러한 다수의 스코는 하나의 스코어로 감소될 필요가 있다. 


기본적으로 내포된 도큐먼트의 매칭 스코어는 평균이다. 이것은 score_mode의 파라미터로 조정될수 있는데 . avg, max, sum, none이 그것이다.


GET /my_index/blogpost/_search

{

  "query": {

    "bool": {

      "must": [

        { "match": { "title": "eggs" }},

        {

          "nested": {

            "path":       "comments",

            "score_mode": "max", 

            "query": {

              "bool": {

                "must": [

                  { "match": { "comments.name": "john" }},

                  { "match": { "comments.age":  28     }}

                ]

        }}}}

      ]

}}}


가장 잘 매칭된 내포 오브젝트로부터 _score를 루트 오브젝트에 준다.


내포된 필터에 의한 결과 자신은 캐쉬되지 않지만 일반적은 캐싱 룰에 의해 내포 필터 내부에 필터에 의해 적용된다.


Sorting by Nested Fields


내포된 필드의 값으로 정렬하는 것이 가능하다. 결과를 더욱 재밌게 만들어보자. 


PUT /my_index/blogpost/2

{

  "title": "Investment secrets",

  "body":  "What they don't tell you ...",

  "tags":  [ "shares", "equities" ],

  "comments": [

    {

      "name":    "Mary Brown",

      "comment": "Lies, lies, lies",

      "age":     42,

      "stars":   1,

      "date":    "2014-10-18"

    },

    {

      "name":    "John Smith",

      "comment": "You're making it up!",

      "age":     28,

      "stars":   2,

      "date":    "2014-10-16"

    }

  ]

}


우리는 10월에 받은 코멘트를 가진 블로그를 검색하기를 원한다. star 갯수가 작은순으로 각 블로그 포스트에 정렬되기를 원한다. 요청은 다음과 같을 것이다.


GET /_search

{

  "query": {

    "nested": { (1)

      "path": "comments",

      "filter": {

        "range": {

          "comments.date": {

            "gte": "2014-10-01",

            "lt":  "2014-11-01"

          }

        }

      }

    }

  },

  "sort": {

    "comments.stars": { (2)

      "order": "asc",   (3)

      "mode":  "min",  (4)

      "nested_filter": { (5)

        "range": {

          "comments.date": {

            "gte": "2014-10-01",

            "lt":  "2014-11-01"

          }

        }

      }

    }

  }

}


(1) 내포된 쿼리는 블로그 포스트의 조회 결과를 10월에 코멘트를 받은 것으로 한정한다.

(2) 결과는 comments.stars 필드의 가장 작은 값으로 오름차순 정렬된다.

(3) 소트 절의 내포된 필터는 메인 쿼리 절의 필터와 동일하다.


왜 우리는 내포된 필터의 쿼리 조건을 반복해야 될까? 이유는 쿼리가 수행된 이후에 정렬이 발생하기 때문이다. 10월에 코멘트를 받은 블로그 포스트가 매치되고, 결과로 블로그 포스트 도큐먼트가 리턴된다. 만약 내포 필터절을 포함하지 않았다면 블로그 포스트가 받은 전체 코멘트에 대한 정렬이 발생하게 될 것이다.


Nested Aggregations


같은 방식으로 우리는 서치 타임에 내포된 오브젝트에 접근하기 위해 특별한 내포 쿼리를 사용할 필요가 있다.


GET /my_index/blogpost/_search

{

  "size" : 0,

  "aggs": {

    "comments": { (1)

      "nested": {

        "path": "comments"

      },

      "aggs": {

        "by_month": {

          "date_histogram": { (2)

            "field":    "comments.date",

            "interval": "month",

            "format":   "yyyy-MM"

          },

          "aggs": {

            "avg_stars": {

              "avg": { (3)

                "field": "comments.stars"

              }

            }

          }

        }

      }

    }

  }

}


(1) 내포된 어그리게이션을 내포된 코멘트 오브젝트로 스텝다운 한다.

(2) comments.date 필드에 기초한 달에 따라 코멘트 버킷에 담는다.

(3) stars 평균값을 계산하여 각 버킷에 담늗다.


결과는 다음과 같은 어그리게이션으로 나타난다.


...

"aggregations": {

  "comments": {

     "doc_count": 4, 

     "by_month": {

        "buckets": [

           {

              "key_as_string": "2014-09",

              "key": 1409529600000,

              "doc_count": 1, 

              "avg_stars": {

                 "value": 4

              }

           },

           {

              "key_as_string": "2014-10",

              "key": 1412121600000,

              "doc_count": 3, 

              "avg_stars": {

                 "value": 2.6666666666666665

              }

           }

        ]

     }

  }

}

...

  

네개의 코멘트가 있으며 하나는 9월 세개는 10월이다.


reverse_nested Aggregation


내포된 어그리게이션은 내포된 도큐먼트의 하나의 필드에만 접근할 수 있다. 이는 루트 도큐먼트나 다른 내포된 도큐먼트의 필드를 볼수 없다는 것이다. 하지만 reverse_nested 어그리게이션을 통해 부모 스코프로 돌아갈 수 있다.


예를 들어 코멘트 나이에 기초한 코멘트의 태그의 관심을 보인다고 가정해보자. comment.age는 내포된 필드인 반면 tags는 루트 도큐먼트에 있다.


GET /my_index/blogpost/_search

{

  "size" : 0,

  "aggs": {

    "comments": {

      "nested": { (1)

        "path": "comments"

      },

      "aggs": {

        "age_group": {

          "histogram": { (2) 

            "field":    "comments.age",

            "interval": 10

          },

          "aggs": {

            "blogposts": {

              "reverse_nested": {}, (3)

              "aggs": {

                "tags": {

                  "terms": { (4)

                    "field": "tags"

                  }

                }

              }

            }

          }

        }

      }

    }

  }

}


(1) 내포된 agg은 코멘트 오브젝트로 스텝 다운한다.

(2) 히스토그램 agg은 comments.age 필드를 그룹핑하고 각 버킷은 10 years이다.

(3) reverse_nested agg는 루트 도큐먼트로 스탭 백한다.

(4) terms agg는 단어들이 나타난 객수로 코멘트를 카운트 한다.


연관된 결과는 다음과 같이 나타난다.


..

"aggregations": {

  "comments": {

     "doc_count": 4, (1)

     "age_group": {

        "buckets": [

           {

              "key": 20, (2)

              "doc_count": 2, 

              "blogposts": {

                 "doc_count": 2, (3)

                 "tags": {

                    "doc_count_error_upper_bound": 0,

                    "buckets": [ (4)

                       { "key": "shares",   "doc_count": 2 },

                       { "key": "cash",     "doc_count": 1 },

                       { "key": "equities", "doc_count": 1 }

                    ]

                 }

              }

           },

...



(1) 4개의 코멘트가 있다. 

(2) 두 개의 코멘트는 20대가 쓴 것이다.

(3) 두 블로그 포스트가 이러한 코멘트에 연관되어 있다.

(4) 이러한 블로그 포스트의 인기 태그는 shares, cash, equlities이다.


When to Use Nested Objects


내포된 오브젝트는 하나의 메인 오브젝트와 적당한 수의 연관된 중요한 엔티티가 있을때 유용한다. 블로그 포스트에 적당한 코멘트에 있을 경우가 그렇다.


내포된 모델의 단점은 다음과 같다.


내포된 도큐먼트를 추가, 변경, 삭제하기 위해서는 전체 도큐먼트를 다시 인덱싱해야 한다. 

검색결과는 전체 도큐먼트가 리턴되며 내포된 도큐먼트만 매칭할수는 없다.

때때로 메인 도큐먼트와 연관된 엔티티를 완전히 분리할 필요가 있는데 이러한 분리를 위해 parent-child 관계가 제공된다.




Comments