본문 바로가기

Monitoring/Elasticsearch

10. Joining Query

Relation
 
관계형 DB 에는 엔티티간에 키를 설정하여 연결을 설정한다.
 
예를 들어 특정 부서가 어떤 도시에 속해있는지를 관계형 DB 로 표현하면 아래와 같이 스키마를 만들고
 
 
데이터는 아래와 같이 각 부서별 도시를 포인팅하는 방식으로 저장 공간이 효율적으로 사용되며
 
데이터를 읽기 위해서는 두 Table 을 Join Query 하여 그 결과를 얻을수 있다.
 
 
 
 
Denormalized Database
 
NoSQL 은 기본적으로 RDBMS 방식의 관계 (엔티티를 키로 연결하는 것) 를 지원하지 않는다. 
 
따라서 관계를 비정규화 하는 방식으로 데이터를 저장한다.
 
예를 들어 아래의 category 를 비정규화 하면 User_messages 에 category 이름을 직접 저장함으로써 
 
User_messages 테이블에서 카테고리 명을 직접 검색할 수 있게 된다.
 
 
 
 
 
Nested Field
 
Elasticsearch 는 NoSQL 이기 때문에 
 
위에서 설명한 반정규화 / 중첩 객체 방식으로 데이터를 저장하는 것을 권장하고 있다.
 
결국 관계를 비정규화 하면 다른 테이블의 정보를 object 로 저장하게 되며
 
이를 nested field ( 중첩필드 ) 라고 한다.
 
이는 아래와 같이 기존 document 에 다른 document 의 정보중
 
필요한 정보를 object 로 중첩 저장하는 것이다.
 
 
앞으로의 테스트를 위해 아래와 같이 index 를 만들어 저장해 보자.
 
PUT /department
{
  "mappings":{
    "_doc":{
      "properties" :{
        "name":{
          "type":"text"
        },
        "employees":{
          "type":"nested"
        }
      }
    }
  }
}
 
POST /department/_doc/1
{
  "name":"Devlopment",
  "employees": [
    {
      "name":"Eric Green",
      "age":39,
      "gender":"M",
      "position":"Big Data Specialist"
    },
    {
       "name":"James Taylor",
      "age":27,
      "gender":"M",
      "position":"Software Developer"     
    },
    {
      "name":"Gray Jenkins",
      "age":21,
      "gender":"M",
      "position":"Intern"      
    },
    {
       "name":"Julie Powell",
      "age":26,
      "gender":"F",
      "position":"Intern"     
    },
    {
       "name":"Benjamin Smith",
      "age":46,
      "gender":"M",
      "position":"Senior Software Engineer"     
    }
  ]
}
 
 
 
Query nested objects
 
중첩 필드에 속해있는 데이터를 검색하기 위해서는 쿼리시 
 
nested 옵션 사용해야 하며 nested.path 에 검색할 object 를 지정하면 된다.
 
GET /department/_search
{
  "query":{
    "nested": {
      "path": "employees",
      "query": {
        "bool":{
          "must":[
            {
             "match":{
               "employees.position": "intern"
             }
            }  
          ],
          "filter":[
            {
             "term":{
               "employees.gender.keyword": {
                 "value":"F"
               }
             }
            }  
          ]
        }        
      }
    }
  }
}
{
  "took" : 4,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 1.1005893,
    "hits" : [
      {
        "_index" : "department",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.1005893,
        "_source" : {
          "name" : "Devlopment",
          "employees" : [
            {
              "name" : "Eric Green",
              "age" : 39,
              "gender" : "M",
              "position" : "Big Data Specialist"
            },
            {
              "name" : "James Taylor",
              "age" : 27,
              "gender" : "M",
              "position" : "Software Developer"
            },
            {
              "name" : "Gray Jenkins",
              "age" : 21,
              "gender" : "M",
              "position" : "Intern"
            },
            {
              "name" : "Julie Powell",
              "age" : 26,
              "gender" : "F",
              "position" : "Intern"
            },
            {
              "name" : "Benjamin Smith",
              "age" : 46,
              "gender" : "M",
              "position" : "Senior Software Engineer"
            }
          ]
        }
      }
    ]
  }
}
 
 
 
 
Nested inner hits
 
위 검색의 결과에는 employees 중 match 된 documents 만 보기가 힘들다.
 
이런 경우 inner hits 옵션을 사용한다.
 
GET /department/_search
{
  "query":{
    "nested": {
      "path": "employees",
      "inner_hits":{},
      "query": {
        "bool":{
          "must":[
            {
             "match":{
               "employees.position": "intern"
             }
            }  
          ],
          "filter":[
            {
             "term":{
               "employees.gender.keyword": {
                 "value":"F"
               }
             }
            }  
          ]
        }        
      }
    }
  }
}
{
  "took" : 9,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 1.1005893,
    "hits" : [
      {
        "_index" : "department",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.1005893,
        "_source" : {
          "name" : "Devlopment",
          "employees" : [
            {
              "name" : "Eric Green",
              "age" : 39,
              "gender" : "M",
              "position" : "Big Data Specialist"
            },
            {
              "name" : "James Taylor",
              "age" : 27,
              "gender" : "M",
              "position" : "Software Developer"
            },
            {
              "name" : "Gray Jenkins",
              "age" : 21,
              "gender" : "M",
              "position" : "Intern"
            },
            {
              "name" : "Julie Powell",
              "age" : 26,
              "gender" : "F",
              "position" : "Intern"
            },
            {
              "name" : "Benjamin Smith",
              "age" : 46,
              "gender" : "M",
              "position" : "Senior Software Engineer"
            }
          ]
        },
        "inner_hits" : {
          "employees" : {
            "hits" : {
              "total" : 1,
              "max_score" : 1.1005893,
              "hits" : [
                {
                  "_index" : "department",
                  "_type" : "_doc",
                  "_id" : "1",
                  "_nested" : {
                    "field" : "employees",
                    "offset" : 3
                  },
                  "_score" : 1.1005893,
                  "_source" : {
                    "name" : "Julie Powell",
                    "age" : 26,
                    "gender" : "F",
                    "position" : "Intern"
                  }
                }
              ]
            }
          }
        }
      }
    ]
  }
}
 
 
위와 같이 employees 중 1개가 match 되었고 array 중 3번째에 위치해 있고
 
_souce 에는 match 된 employee 를 보여주며 _socre 기준으로 sort 하여 표시해 준다.
 
 
 
Document releation mapping
 
이런 방식은 결국 department document 에 employees 정보가 저장되는 구조이므로 
 
employee 의 정보만 업데이트 하기 힘들게 된다.
 
Elasticsearch 에서는 NoSQL 임에도 불구하고 child/parent 를 설정하여
 
관계를 mapping 할 수 있는 document relation mapping 이라는 기능을 제공한다.
 
다만 이런 관계형 데이터는 여러 shard 에 저장할 수 없으며 같은 shard 에 있어야 한다고 한다. 
 
 
 위의 경우라면 아래와 같이 type 이 join 이고 relations 을 설정하는 필드를 하나 제공해 주면 된다.
 
DELETE /department 
 
PUT /department
{
  "mappings":{
    "_doc":{
      "properties" :{
        "join_field":{
          "type":"join",
          "relations":{
            "department":"employee"
          }
        }
      }
    }
  }
}
 
 
 
Adding documents with releation
 
값은 아래와 같이 입력해주면 된다.
 
주목할 부분은 routing 입력과 join_field.parent 부분이다.
 
routing 이란 주어진 document 가 어떤 shard 에 속해있는지
 
확인하거나 새 document 를 indexing 할때 사용하며
 
기존 document 를 찾을때도 사용하는데 입력값으로
 
document 의 id 정보를 넣어주면 속한 shard 정보를 리턴해준다.
 
이 정보가 필요한 이유는 join 의 경우 해당 object 를 같은 shard 에 저장해야 하기 때문에
 
이 정보를 제공해야 한다고 한다. 
 
PUT /department/_doc/1
{
  "name":"Development",
  "join_field":"department"
}
 
 
PUT /department/_doc/2
{
  "name":"Marketing",
  "join_field":"department"
}
 
PUT /department/_doc/3?routing=1
{
  "name":"Bo Anderson",
  "age":28,
  "gender":"M",
  "join_field": {
    "name":"employee",
    "parent":1
  }
}
 
PUT /department/_doc/4?routing=2
{
  "name":"John Doe",
  "age":44,
  "gender":"M",
  "join_field": {
    "name":"employee",
    "parent":2
  }
}
 
PUT /department/_doc/5?routing=1
{
  "name":"jane Park",
  "age":23,
  "gender":"F",
  "join_field": {
    "name":"employee",
    "parent":1
  }
}
 
PUT /department/_doc/6?routing=2
{
  "name":"Christina Parker",
  "age":29,
  "gender":"F",
  "join_field": {
    "name":"employee",
    "parent":2
  }
}
 
 
 
Query child document by parent ID
 
employees 중 특정 document 와 관계를 맺은 employee documents 를 검색시에는 아래와 같이 하면 된다.
 
GET /department/_search
{
  "query":{
    "parent_id":{
      "type":"employee",
      "id":1
    }
  }
}
{
  "took" : 6,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 2,
    "max_score" : 0.13353139,
    "hits" : [
      {
        "_index" : "department",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.13353139,
        "_routing" : "1",
        "_source" : {
          "name" : "Bo Anderson",
          "age" : 28,
          "gender" : "M",
          "join_field" : {
            "name" : "employee",
            "parent" : 1
          }
        }
      },
      {
        "_index" : "department",
        "_type" : "_doc",
        "_id" : "5",
        "_score" : 0.13353139,
        "_routing" : "1",
        "_source" : {
          "name" : "jane Park",
          "age" : 23,
          "gender" : "F",
          "join_field" : {
            "name" : "employee",
            "parent" : 1
          }
        }
      }
    ]
  }
}
 
 
 
Query child document by parent
 
child document 검색시 parent 의 document 를 모르고
 
parent 의 특정 정보를 아는 경우 아래와 같이 query 하면 된다.
 
GET /department/_search
{
  "query":{
    "has_parent":{
      "parent_type":"department",
      "query":{
        "term": {
          "name.keyword": "Development"
        }
      }
    }
  }
}
{
  "took" : 8,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 2,
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "department",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_routing" : "1",
        "_source" : {
          "name" : "Bo Anderson",
          "age" : 28,
          "gender" : "M",
          "join_field" : {
            "name" : "employee",
            "parent" : 1
          }
        }
      },
      {
        "_index" : "department",
        "_type" : "_doc",
        "_id" : "5",
        "_score" : 1.0,
        "_routing" : "1",
        "_source" : {
          "name" : "jane Park",
          "age" : 23,
          "gender" : "F",
          "join_field" : {
            "name" : "employee",
            "parent" : 1
          }
        }
      }
    ]
  }
}
 
 
 
Query parent document by child
 
위 경우와 반대로 child 조건을 포함하고 있는 parent document 를 구해보자.
 
GET /department/_search
{
  "query":{
    "has_child":{
      "type":"employee",
      "query":{
         "range": {
           "age": {
             "gte": 40,
             "lte": 50
           }
         }     
      }
    }
  }
}
{
  "took" : 8,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "department",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "name" : "Marketing",
          "join_field" : "department"
        }
      }
    ]
  }
}
 
추가로 has_child 검색시 검색 결과를 sorting 및 filtering 시 아래 링크와 같이 설정할 수 있다고 한다.
 
 
 
Multi level releations mapping
 
지금까지 단순히  아래와 같이 하나의 relation 이 있는 경우만 고려하였다.
 
 
이번에는 아래와 같은 상황을 한번 생각해 보자.
 
 
이를 위한 맵핑은 간단하다. 아래와 같이 하면 된다.
 
PUT /company
{
  "mappings": {
    "_doc": {
      "properties": {
        "join_field": {
          "type":"join",
          "relations": {
             "company":["department","supplier"],
             "department": "employee"
          }
        }
      }
    }
  }
}
 
 
 
Multi level releations adding documents
 
입력도 이전과 별 반 다르지 않지만
 
약간 주의해서 볼 부분이 routing 부분과 parent 입력 부분이 다른 점이다.
 
PUT /company/_doc/1
{
  "name": "My Company Inc.",
  "join_field": "company"
}
 
 
PUT /company/_doc/2?routing=1
{
  "name": "Development",
  "join_field": {
    "name":"department",
    "parent":1
  }
}
 
PUT /company/_doc/3?routing=1
{
  "name": "Bo Andersen",
  "join_field": {
    "name":"employee",
    "parent":2
  }
}
결과적으로 아래 그림과 같은 상황이 된다.
 
 
 
 
Multi level releations query documents
 
이번에는 테스트를 위해 몇몇 데이터를 더 넣었다.
 
PUT /company/_doc/4
{
  "name": "Another Company, Inc.",
  "join_field": "company"
}
 
PUT /company/_doc/5?routing=4
{
  "name": "Marketing",
  "join_field": {
    "name":"department",
    "parent":4
  }
}
 
PUT /company/_doc/3?routing=4
{
  "name": "John Doe",
  "join_field": {
    "name":"employee",
    "parent":5
  }
}
 
 
이 상태에서 John Doe 가 속해있는 company 를 조회해 보자.
 
마찬가지로 has_child 옵션을 사용하면 되는데
 
검색순서가 employee->department->company 순으로 되야 하므로
 
아래와 같은 query 를 만들수 있으며 안쪽부터 query 가 시작된다고 생각하면 되겠다.
 
GET /company/_search
{
  "query":{
    "has_child":{
      "type":"department",
      "query":{
        "has_child": {
          "type": "employee",
          "query": {
            "term": {
              "name.keyword": "John Doe"
            }
          }
        }
      }
    }
  }
}
{
  "took" : 6,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "company",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 1.0,
        "_source" : {
          "name" : "Another Company, Inc.",
          "join_field" : "company"
        }
      }
    ]
  }
}
 
 
 
 
Relations inner hits
 
일치 documents 정보를 상세히 보는 방법은 Nested 일때와 다르지 않다.
 
보고자 하는 documents 에 inner_hits 를 넣으면 된다.
 
GET /company/_search
{
  "query":{
    "has_child":{
      "type":"department",
      "inner_hits": {},
      "query":{
        "has_child": {
          "type": "employee",
          "inner_hits": {},
          "query": {
            "term": {
              "name.keyword": "John Doe"
            }
          }
        }
      }
    }
  }
}
{
  "took" : 17,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "company",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 1.0,
        "_source" : {
          "name" : "Another Company, Inc.",
          "join_field" : "company"
        },
        "inner_hits" : {
          "department" : {
            "hits" : {
              "total" : 1,
              "max_score" : 1.0,
              "hits" : [
                {
                  "_index" : "company",
                  "_type" : "_doc",
                  "_id" : "5",
                  "_score" : 1.0,
                  "_routing" : "4",
                  "_source" : {
                    "name" : "Marketing",
                    "join_field" : {
                      "name" : "department",
                      "parent" : 4
                    }
                  },
                  "inner_hits" : {
                    "employee" : {
                      "hits" : {
                        "total" : 1,
                        "max_score" : 0.9808292,
                        "hits" : [
                          {
                            "_index" : "company",
                            "_type" : "_doc",
                            "_id" : "3",
                            "_score" : 0.9808292,
                            "_routing" : "4",
                            "_source" : {
                              "name" : "John Doe",
                              "join_field" : {
                                "name" : "employee",
                                "parent" : 5
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              ]
            }
          }
        }
      }
    ]
  }
}
 

'Monitoring > Elasticsearch' 카테고리의 다른 글

12. Aggregation  (0) 2020.01.17
11. Query Result Options  (0) 2020.01.17
09. Compound Query  (0) 2020.01.17
08. Query  (0) 2020.01.17
07. Analyzer  (1) 2020.01.17