ElasticSearch 정리 5일차 - type

0

elasticsearch

목록 보기
5/9

type

Mapping and field type

mapping은 document가 index에 어떻게 색인되고 저장되는 지 정의하는 부분이다. JSON문서의 각 필드를 어떤 방식으로 분석하고 색인할 지, 어떤 타입으로 저장할 지 등을 세부적으로 지정할 수 있다.

kibana devtools를 열어서, my_index2 index를 하나 만들어보도록 하자.

PUT my_index2

다음으로 my_index2에 document를 하나 만들고, mapping부분을 확인해보도록 하자.

PUT my_index2/_doc/1
{
  "title": "hello world",
  "views": 1234,
  "public": true,
  "point": 4.5,
  "created": "2019-01-17T14:05:01.234Z"
}

GET my_index2

다음의 결과가 나온다.

{
  "my_index2" : {
    "aliases" : { },
    "mappings" : {
      "properties" : {
        "created" : {
          "type" : "date"
        },
        "point" : {
          "type" : "float"
        },
        "public" : {
          "type" : "boolean"
        },
        "title" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "views" : {
          "type" : "long"
        }
      }
    },
    "settings" : {
      
    }
  }
}

document의 각 필드 아래에 type으로 필드 타입이 생긴 것을 확인할 수 있다. 이렇게 index에 document가 색인될 때 기존에 mapping 정보를 가지고 있지 않던 새로운 필드가 들어오면 elasticsearch는 자동으로 document의 content를 보고 적당한 필드 type을 지정해서 mapping 정보를 생성한다.

Dynamic mapping vs Explicit mapping

elasticsearch가 자동으로 mapping을 생성하는 것을 동적 매핑(Dynamic mapping), 사용자가 직접 mapping을 지정해주면 명시적 매핑(explicit mapping)이라고 한다. 다음과 같이 index를 생성할 때, explicit하게 mapping해보도록 하자.

PUT mapping_test
{
  "mappings": {
    "properties": {
      "createdData": {
        "type": "date",
        "format": "strict_date_time || epoch_millis"
      },
      "keywordString": {
        "type": "keyword"
      },
      "testString": {
        "type": "text"
      }
    }
  },
  "settings": {
    "number_of_replicas": 1,
    "number_of_shards": 1
  }
}

다음의 응답이 나오면 성공이다.

{
  "acknowledged" : true,
  "shards_acknowledged" : true,
  "index" : "mapping_test"
}

위 예제는 document의 각 field에 data를 어떠한 형태로 저장할 것인지를 type이라는 설정으로 지정했다. 구체적으로 이 값들이 어떤 의미를 가지는 지는 나중에 알아보도록 하자. 중요한 것은 필드 type을 포함한 mapping 설정 내 대부분이 한 번 지정되면 사실상 변경이 불가능하다라는 것이다.

위와 같은 이유로, 서비스 운영 환경에서 대용량 데이터를 처리해야할 때는 기본적으로 명시적으로 mapping을 지정해서 index를 운영해야한다. mapping을 어떻게 지정하느냐에 따라 서비스 운영 양상이 많이 달라지며 성능의 차이도 크다.

이미 인덱스가 생성된 경우에도 신규 field를 추가할 때에는 mapping 정보를 추가할 수 있다. 다음과 같다.

PUT mapping_test/_mapping
{
  "properties": {
    "longValue": {
      "type": "long"
    }
  }
}

다음으로 잘 추가되었는 지 확인해보도록 하자.

GET mapping_test/_mapping

{
  "mapping_test" : {
    "mappings" : {
      "properties" : {
        "createdData" : {
          "type" : "date",
          "format" : "strict_date_time || epoch_millis"
        },
        "keywordString" : {
          "type" : "keyword"
        },
        "longValue" : {
          "type" : "long"
        },
        "testString" : {
          "type" : "text"
        }
      }
    }
  }
}

잘 field가 추가된 것을 볼 수 있다.

Field 타입

elasticsearch에서 field type은 다음과 같이 구분이 가능하다.

분류종류
Simple typetext, keyword, date, long, double, boolean, ip 등
계층 구조 typeobject, nested 등
특수 타입geo_point, geo_shape 등

simple type은 직관적으로 알기 쉬운 간단한 자료형이다. boolean, long, double등은 숫자를, text, keyword는 문자열을 표현한다. date는 날짜, ip는 IP정보를 표현한다. textkeyword는 추후에 더 알아보도록 하자.

숫자 타입은 다음과 같다.

종류설명
long64비트 정수
integer32비트 정수
short16비트 정수
byte8비트 정수
double64비트 부동소수점 수
float32비트 부동소수점 수
half_float16비트 부동소수점 수
scaled_floatdouble 고정 환산 계수로 스케일링하여 long으로 저장되는 부동소수점 수

date 타입은 시간과 관련된 데이터를 편리하게 색인하고 검색하기 위해 도입된 타입이다. 위의 예시를 다시 살펴보면,

{
  "mappings": {
    "properties": {
      "createdData": {
        "type": "date",
        "format": "strict_date_time || epoch_millis"
      },

format이 두 가지 중 하나로 strict_date_timeepoch_millis인 것을 알 수 있다.

다음의 두 가지 document를 index에 추가할 수 있다.

PUT mapping_test/_doc/1
{
  "createdDate": "2020-09-03T02:41:32.001Z"
}

PUT mapping_test/_doc/2
{
  "createdDate": 1599068514123
}

다음으로 elasticsearch의 range query를 이용하여 range 경계값 사이에 해당하는 document를 가져오도록 하자.

GET mapping_test/_search
{
  "query": {
    "range": {
      "createdDate": {
        "gte": "2020-09-02T17:00:00.000Z",
        "lte": "2020-09-03T03:00:00.000Z"
      }
    }
  }
}

유념할 것은 elasticsearch는 index가 refresh되어 in-memory에서 flush되어야 search가 가능하다는 것에 유념하자.

결과를 확인하면 다음과 같다.

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "mapping_test",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "createdDate" : 1599068514123
        }
      },
      {
        "_index" : "mapping_test",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "createdDate" : 1599068514123
        }
      }
    ]
  }
}

같은 서로 다른 형식의 date이지만 모두 검색이 가능한 것을 알 수 있다.

elasticsearch는 date의 format을 옵션으로 지정할 수 있는데, format은 여러 형식으로 지정할 수 있어 document가 어떤 형식으로 들어오더라도 elasticsearch 내부적으로는 UTC시간대로 변환하는 과정을 거쳐 epoch milliseconds형식의 long 숫자로 색인할 수 있다.

다양한 date타입 forma들이 있는데 다음과 같다.

종류설명
epoch_millis밀리초 단위로 표현한 epoch시간
epoch_second초 단위로 표현한 epoch 시간
date_timeyyyyMMdd 형태로 표현한 날짜
strict_date_timeyyyy-MM-dd'T'HH:mm:ss.SSSZZ로 표현한 날짜와 시간
date_optional_time최소 연 단위의 날짜를 포함, 선택적으로 시간정보도 포함, ISO datetime 형태로 표현된 날짜와 시간 ex) yyyyy-MM-dd 또는 yyyy-MM-dd'T'HH:mm:ss.SSSZ
strict_date_optional_timedate_optional_time과 동일하지만 연,월,일이 각각 4자리 2자리 2자리 임을 보장한다.

배열

elasticsearch에는 배열을 표현하는 별도의 type이 없다. 즉, long type이라도 [221, 309, 1599208568]과 같은 배열을 넣을 수 있다. 다음과 같이 index mapping을 지정하여 index를 생성해보고 test해보도록 하자.

PUT array_test
{
  "mappings": {
    "properties": {
      "longField": {
        "type": "long"
      },
      "keywordField": {
        "type": "keyword"
      }
    }
  }
}

이제 다음의 두 document를 색인해보도록 하자.

PUT array_test/_doc/1
{
  "longField": 309,
  "keywordField": ["hello", "world"]
}

PUT array_test/_doc/2
{
  "longField": [221, 309, 1599208568],
  "keywordField": "hello"
}

두 요청 모두 성공하는 것을 볼 수 있을 것이다.

단, 하나의 field에 여러 타입이 혼합되는 경우는 불가능하다. 가령 longField[309, "hello"]를 배열로 넣을 수는 없다. 배열은 지정한 타입이 맞아야만 가능한 것이다.

계층 구조를 지원하는 타입

field 하위에 다른 field가 들어가는 계층 구조의 데이터를 담는 타입으로 objectnested가 있다. 이 둘은 매무 유사하나 배열을 처리할 때의 동작이 다르다.

먼저 object타입은 json객체와 비슷하다고 생각하면 된다.

PUT object_test/_doc/1
{
  "price": 2770.75,
  "spec": {
    "cores": 12,
    "memory": 128,
    "storage": 8000
  }
}

object_test index에 1번 document로 위의 data를 넣었다. spec field가 하위 field를 포함하는 object임을 알 수 있다.

동적 생성된 object_test index의 mapping 정보를 확인해보면 다음과 같다.

{
  "object_test" : {
    "aliases" : { },
    "mappings" : {
      "properties" : {
        "price" : {
          "type" : "float"
        },
        "spec" : {
          "properties" : {
            "cores" : {
              "type" : "long"
            },
            "memory" : {
              "type" : "long"
            },
            "storage" : {
              "type" : "long"
            }
          }
        }
      }
    },
    ...
  }
}

응답을 살펴보면 spec field의 type이 명시적으로 object라고 표현되지 않았다. 이는 object타입이 default이기 때문이다.

nested타입과 object타입의 배열 처리 동작이 다르다고 했다. 먼저 object 타입의 배열 처리가 어떻게되는 지 알아보도록 하자.

PUT object_test/_doc/2
{
  "spec": [
    {
      "cores": 12,
      "memory": 128,
      "storage": 8000
    },
    {
      "cores": 6,
      "memory": 64,
      "storage": 8000
    },
    {
      "cores": 6,
      "memory": 32,
      "storage": 8000
    }
  ]
}

spec에 배열을 만들고 내부에 object들로 가득 채웠다.

이제 spec.cores가 6개이며 spec.memory는 128인 document를 검색해보도록 하자. _search 문을 사용하며 query.boolmust절에 각 query들을 나열하면 AND조건으로 연결된다.

GET object_test/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "spec.cores": "6"
          }
        },
        {
          "term": {
            "spec.memory": "128"
          }
        }
      ]
    }
  }
}

예상대로라면 cores6이고 memory128spec element는 없다. 그러나 결과를 확인해보면 다르다.

...
  "hits" : {
      "total" : {
        "value" : 1,
        "relation" : "eq"
      },
      "max_score" : 2.0,
      "hits" : [
        {
          "_index" : "object_test",
          "_type" : "_doc",
          "_id" : "2",
          "_score" : 2.0,
          "_source" : {
            "spec" : [
              {
                "cores" : 12,
                "memory" : 128,
                "storage" : 8000
              },
              {
                "cores" : 6,
                "memory" : 64,
                "storage" : 8000
              },
              {
                "cores" : 6,
                "memory" : 32,
                "storage" : 8000
              }
            ]
          }
        }
      ]
    }
  }
...

결과부분인 hits_source.spec을 보면 3개의 object들이 있는 것을 볼 수 있다. 위에서 쓴 query문이 AND조건으로 검색된 것이 아니라, OR조건으로 검색된 것을 확인할 수 있다. 왜 그런가???
사실 object타입의 배열은 배열을 구성하는 객체 데이터를 서로 독립적인 데이터로 취급하지 않기 때문이다. 즉, spec안의 object들이 모여서 하나의 spec객체를 이룬다고 생각하면 된다.

그러나 어떤 경우에는 이들이 꼭 독립적인 데이터로 취급되어야 할 필요가 있다. 이러한 문제를 해결하기 위해서 nested타입이 도입된 것이다.

nested타입은 object타입과 다르게 배열 내 각 객체를 독립적으로 취급한다.

먼저 nested_test index를 만들어주도록 하자.

PUT nested_test
{
  "mappings": {
    "properties": {
      "spec": {
        "type": "nested",
        "properties": {
          "cores": {
            "type": "long"
          },
          "memory": {
            "type": "long"
          },
          "storage": {
            "type": "long"
          }
        }
      }
    }
  }
}

nested_testnested타입이며 각 properties는 cores, memory, storage를 갖는다. 이제 document를 하나 만들어보도록 하자.

PUT nested_test/_doc/1
{
  "spec": [
    {
      "cores": 12,
      "memory": 128,
      "storage": 8000
    },
    {
      "cores": 6,
      "memory": 64,
      "storage": 8000
    },
    {
      "cores": 6,
      "memory": 32,
      "storage": 8000
    }
  ]
}

이제 object_test index를 만들었던 것처럼 document를 만들었다. 다음으로 이전과 동일한 _search query를 동작시키도록 하자.

GET nested_test/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "spec.cores": "6"
          }
        },
        {
          "term": {
            "spec.memory": "128"
          }
        }
      ]
    }
  }
}

결과로 아무것도 검색되지 않는 것을 볼 수 있다. spec field를 nested타입으로 선언하였기 때문에 우리가 예상한 결과를 얻어낸 것 같지만 실제로는 조금 다르다.

다음과 같이 spec.memory128로 변경해서 _search해보도록 하자.

GET nested_test/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "spec.cores": "6"
          }
        },
        {
          "term": {
            "spec.memory": "64"
          }
        }
      ]
    }
  }
}

분명 spec.cores가 6이고 spec.memory64인 경우가 있는데, 해당 데이터가 검색되지 않는다. 왜일까??

nested 타입은 객체 배열의 각 객체를 내부적으로 별도의 lucene document로 문리해 저장한다. 배열의 원소가 100개라면 부모 document까지 포함해서 101개의 lucene document가 내부적으로 생성된다. nested의 이런 동작은 엘리스틱서치 내에서 굉장히 특수하기 때문에 nested query라는 전용 쿼리를 이용해서 검색해야한다.

GET nested_test/_search
{
  "query": {
    "nested": {
      "path": "spec",
      "query": {
        "bool": {
          "must": [
            {
              "term": {
                "spec.cores": "6"
              }
            },
            {
              "term": {
                "spec.memory": "64"
              }
            }
          ]
        }
      }
    }
  }
}

nested라는 query를 만들었고 path에 검색할 nested타입 field의 property를 적어주면 된다. 우리의 경우는 spec이므로 pathspec을 적었고 nested.query부분은 이전과 동일하다. 이렇게 해주면 nested배열 타입을 가지는 spec안에서 spec.cores가 6이면서 spec.memory가 64인 document를 뽑아낸다.

"hits" : [
      {
        "_index" : "nested_test",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 2.0,
        "_source" : {
          "spec" : [
            {
              "cores" : 12,
              "memory" : 128,
              "storage" : 8000
            },
            {
              "cores" : 6,
              "memory" : 64,
              "storage" : 8000
            },
            {
              "cores" : 6,
              "memory" : 32,
              "storage" : 8000
            }
          ]
        }
      }
    ]

성공적으로 배열 내의 nested타입을 가지는 개별 객체들을 검색하여 일치하면 해당 document를 위와 같이 전달한다. 만약, spec.memory128이면 object때와 다르게 실패할 것이다.

참고로, 위에서 설명한 것과 같이 nested타입은 내부적으로 각 객체를 별도의 lucene document로 분리해서 저장하기 때문에 성능 문제가 있을 수 있다. 따라서 elasticsearch는 nested 타입의 무분별한 사용을 막기 위해 index설정으로 두 가지 제한을 걸어 놓았다. index.mapping.nested_fields.limit 설정은 한 index에 nested 타입을 몇 개까지 지정할 수 있는 지를 제한한다. default는 50이다. 또, index.mapping.nested_objects.limit은 한 document가 nested객체를 몇 개까지 가질 수 있는 지를 제한한다. 기본값은 10000이다. 이처럼 nested는 무분별하게 사용할 경우 OOM의 위험이 있다.

이외의 타입

엘라스틱서치 인덱스의 field 타입에는 다양한 비지니스 요구사항을 위한 타입들이 있다.

종류설명
geo_point위도와 경도를 저장하는 타입
geo_shape지도상에 특정 지점이나 선, 도형 등을 표현하는 타입
binarybase64로 인코딩된 문자열을 저장하는 타입
long_range, date_range, ip_range 등경계값을 지정하는 방법 등을 통해 수, 날짜, IP 등의 범위를 저장하는 타입
completion자동완성 검색을 위한 특수 타입

0개의 댓글