티스토리 뷰

이번 포스팅은 사내에서 Elasticsearch 관련 내용 발표를 위해 "시작하세요! 엘라스틱서치"서적을 기반으로 학습하고 이해한 내용을 정리하는 포스팅이다. Elasticsearch 역시 내용이 많기 때문에 시리즈로 나눠서 정리할 예정이다. 모든 내용은 Elasticsearch 7.6 버전 기준이다.

 

오늘은 Elasticsearch 의 Mapping 에 대해서 알아볼 예정이다.  

매핑(Mapping)

Mapping 은 Elasticsearch 에서 데이터의 저장 형태와 검색을 위해 해당 데이터를 어떻게 접근하고 처리하는지에 대한 명세이다. Elasticsearch 에서 Mapping 을 설정하는 방법은 크게 2가지가 있다. 첫 번째는 Index 를 생성하면서 Mapping 을 설정하는 방법이고, 두 번째는 _mapping API 를 통해서 설정하는 방법이다. 

다음은 신규 Index 를 생성하면서 Mapping 을 설정하는 예제이다. 신규 Index 가 생성되면서 weigth 라는 필드는 앞으로 long 타입의 데이터를 다루게 된다. 

curl -H 'Content-Type: application/json' -X PUT 'localhost:9200/mapping_user_bulk?pretty' -d '
{
  "mappings":{
    "properties":{
      "weight" : {
        "type" : "long"
      }
    }
  }
}'
--- 반환값 ---
{
  "acknowledged" : true,
  "shards_acknowledged" : true,
  "index" : "mapping_user_bulk"
}

 또 다른 방법으로 _mapping API 를 이용하면 Document 에 입력된 데이터의 Mapping 형태를 확인 및 정의할 수 있다. 기존에 사용하던 user_bulk Index 의 Mapping 정보를 확인해보자. 

curl -H 'Content-Type: application/json' -X GET 'localhost:9200/user_bulk/_mapping?pretty'

--- 반환값 ---
{
  "user_bulk" : {
    "mappings" : {
      "properties" : {
        "birth" : {
          "type" : "long"
        },
        "country" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "first_name" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "last_name" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "zip" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        }
      }
    }
  }
}

 

 Elasticsearch 는 생성되지 않은 Index 에 처음 데이터를 입력하면 입력된 데이터의 형식을 참고해서 자동으로 Mapping 된다. 우리는 이전에 user_bulk Index 를 만들 때 별도로 Mapping 을 설정하지 않았지만 입력되는 데이터 값에 따라 타입이 자동으로 설정된 것을 확인할 수 있다. 주의할 점은 데이터 값이 날짜 형식일 경우 날짜 시간 형식으로 (yyyy/MM/dd HH:mm:ss 혹은 yyyy/MM/dd 등) 되어있어야지만 Date 타입으로 Mapping 을 설정한다.

 또한 기존에 생성된 Index 에 Mapping 정보를 추가할 수도 있다. PUT 메소드를 사용하고 properties 필드에 필드명과 타입 등을 옵션으로 입력해서 설정할 수 있다. 

# 기존의 Index 에 신규 Mapping 정보 추가 
curl -H 'Content-Type: application/json' -X PUT 'localhost:9200/user_bulk/_mapping?pretty' -d '
{
    "properties":{
      "new_mapping_field" : {
        "type" : "long"
      }
    }
}'
--- 반환값 ---
{  "acknowledged" : true  }


# Mapping 조회 
curl -H 'Content-Type: application/json' -X GET 'localhost:9200/user_bulk/_mapping?pretty'
{
  "user_bulk" : {
    "mappings" : {
      "properties" : {
        "birth" : {
          "type" : "long"
        },
        ...
        "new_mapping_field" : {
          "type" : "long"
        },
        "zip" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        }
      }
    }
  }
}

 한 가지 명심할 점은 한번 설정된 Mapping 에 필드를 추가할 수는 있지만 수정, 삭제는 불가능하다니 Mapping 을 설정할 때는 항상 주의하자. 이에 대한 자세한 내용은 여기 에서 확인할 수 있다. 

 

데이터 타입

위의 예제를 통해 _mapping API 를 이용해서 데이터 타입을 지정할 수 있는 것을 알게 되었다. 그러면 이제는 우리가 사용할 수 있는 데이터 타입과 타입들이 옵션이 어떤 것들이 있는지 알아보자. 데이터 타입에 대한 자세한 내용은 여기 에서 확인할 수 있다. 

1. 문자열(string)

문자열은 text 와 keyword 타입으로 나뉜다.

  • text
     메일의 본문내용, 제품의 설명 등 전문검색(full-text-value) 을 위해서 analyzer 에 의해 분리된 텀(term) 들을 사용하는 필드이다. 데이터가 색인(indexing) 되기 전에 analyzer 를 통해서 여러 개의 텀(term) 들로 변경되고 저장된다. 이때 쪼개진 term 들을 이용해서 Elasticsearch 는 전체 텍스트 필드 중에서 개별 단어들을 검색할 수 있게 된다.
  • keyword
     ID 나 CODE, 우편번호처럼 완전 일치 검색을 할 때 사용되는 필드이다. 일반적으로 Filter, Sort, Aggregation 등에 사용된다. 
    별도의 analyzer 로 형태소 분석을 하지 않고 원본 데이터를 그대로 token 으로 다룬다.
    알아두면 좋은 옵션은 ignore_above 이다.  ignore_above 는 특정 길이를 초과하는 데이터는 검색을 위한 token 으로 저장하지 않게한다. 
# 신규 Index 생성 및 long_string 필드는 keyword 로 지정 및 4글자 초과의 문자열이 올경우 token 을 저장하지 않게 설정.
curl -H 'Content-Type: application/json' -X PUT 'localhost:9200/ignore_above_sample?pretty' -d '
{
  "mappings":{
    "properties":{
      "long_string" : {
        "type" : "keyword",
        "ignore_above": 4
      }
    }
  }
}'

# 신규 데이터 추가 
curl -H 'Content-Type: application/json' -X POST 'localhost:9200/ignore_above_sample/_doc/1' -d ' { "long_string":"hi" }'
curl -H 'Content-Type: application/json' -X POST 'localhost:9200/ignore_above_sample/_doc/2' -d ' { "long_string":"hello" }'

# 데이터 검색 2글자
curl -H 'Content-Type: application/json' -X GET 'localhost:9200/ignore_above_sample/_search?pretty' -d '
{ "query": { "term" : { "long_string": "hi" } } }'
--- 반환값 ---
{
  ...
    "hits" : [
      {
        "_index" : "ignore_above_sample",
        "_id" : "1",
        "_source" : {
          "long_string" : "hi"
        }
      }
    ]
  }
}

# 데이터 검색 5글자
curl -H 'Content-Type: application/json' -X GET 'localhost:9200/ignore_above_sample/_search?pretty' -d ' 
{ "query": { "term" : { "long_string": "hello" } } }'
--- 반환값 ---
{
  ...
    "hits" : [ ]
}

 일반적으로 문자열 데이터를 바탕으로 자동으로 mapping 될 경우 string 타입의 데이터는 multi-fields 를 이용해서 text 타입과 keywod 타입을 모두 갖고 있다.

textkeyword 에 대한 자세한 내용은 각각의 링크에서 확인할 수 있다. 

 

2. 숫자(numeric)

 정수형 타입과 실수형 타입을 모두 지원하고 문자열 타입과는 다르게 analyzer 를 이용한 형태소 분석을 하지 않는다. 따라서 숫자 필드는 검색어로 검색이 불가능하며 주어진 기준값과 비교해서 이상 이하 초과 같은 산술식의 형태로 검색한다. 

알아두면 좋은 옵션은 coerce 이다. coerce 는 문자열이 입력되면 숫자로 값을 변환시킨다. 또한 정수형에 실수가 입력되는 경우에는 소수점을 제거한다. false 일 경우 형태가 안 맞는 값이 입력될 경우 오류를 반환한다. 

# 신규 Index 생성 및 coerce_numeric 필드를 integer 타입으로 지정하고 coerce 를 false 로 지정해서 형식이 안맞을경우 오류를 반환하게 지정
curl -H 'Content-Type: application/json' -X PUT 'localhost:9200/coerce_sample?pretty' -d '
{
  "mappings":{
    "properties":{
      "coerce_numeric" : {
        "type" : "integer",
        "coerce": false
      }
    }
  }
}'

# 데이터 추가 숫자 1
curl -H 'Content-Type: application/json' -X POST 'localhost:9200/coerce_sample/_doc/1' -d ' 
{ "coerce_numeric": 1 }'

--- 반환값 ---
{"_index":"coerce_sample","_type":"_doc","_id":"1","_version":1,"result":"created",
"_shards":{"total":2,"successful":1,"failed":0},"_seq_no":0,"_primary_term":1}

# 데이터 추가 문자 2
curl -H 'Content-Type: application/json' -X POST 'localhost:9200/coerce_sample/_doc/2' -d ' 
{ "coerce_numeric": "2" }'

--- 반환값 ---
{ 
  "error":{
    "root_cause":[
      { "type":"mapper_parsing_exception",
        "reason":"failed to parse field [coerce_numeric] of type [integer] in document with id '2'. Preview of field's value: '2'"
      }
    ],
    "type":"mapper_parsing_exception",
    "reason":"failed to parse field [coerce_numeric] of type [integer] in document with id '2'. Preview of field's value: '2'",
    "caused_by":{ 
      "type":"illegal_argument_exception",
      "reason":"Integer value passed as String"
    }
  },
  "status":400
}

numeric 에 대한 자세한 내용은 여기 에서 확인할 수 있다. 

 

3. 날짜(date)

JSON 에는 날짜 형식이 없기 때문에 Elasticsearch 는 다음과 같은 포맷이 올 경우 날짜 형식으로 판단한다. 

  • 날짜 포맷을 포함하는 문자열 "2020-01-01" or "2020/01/01 12:10:30"
  • 밀리세컨드를 표현하는 정수 (필드가 이미 Date 형식으로 Mapping 되어 있을 때)
  • 초를 나타내는 정수  (필드가 이미 Date 형식으로 Mapping 되어 있을 때)
# 자동 Mapping 이 가능하도록 Date 형식으로 요청
curl -H 'Content-Type: application/json' -X POST 'localhost:9200/date_sample/_doc/1' -d ' 
{ "date_format":"2020-05-05" }'

# Mapping 상태 확인
curl -H 'Content-Type: application/json' -X GET 'localhost:9200/date_sample/_mapping?pretty'
--- 반환값 ---
{
  "date_sample" : {
    "mappings" : {
      "properties" : {
        "date_format" : {
          "type" : "date"
        }
      }
    }
  }
}

# Date Range 조회
curl -H 'Content-Type: application/json' -X GET 'localhost:9200/date_sample/_search?pretty' -d ' 
{ "query": { "range" : { "date_format": { "gte" : "2020-05-05", "lt": "2020-05-06" } } } }'

--- 반환값 ---
{
    ...
    "hits" : [
      {
        "_index" : "date_sample",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "date_format" : "2020-05-05"
        }
      }
    ]
  }
}

내부적으로 날짜 포맷을 UTC 타임존 기준으로 밀리세컨드를 표현하는 정수 값으로 저장한다고 한다.

date 에 대한 자세한 내용은 여기 에서 확인할 수 있다. 

 

4 boolean

boolean 타입의 값은 true, false 만 가질 수 있다. boolean 타입으로 Mapping 되어 있을 경우 문자열 "true", "false" 도 해석 가능하다. 

# 신규 Index 생성시에 boolean_format 필드를 boolean 타입으로 지정
curl -H 'Content-Type: application/json' -X PUT 'localhost:9200/boolean_sample?pretty' -d '
{
  "mappings":{
    "properties":{
      "boolean_format" : {
        "type" : "boolean"
      }
    }
  }
}'

# boolean 타입의 데이터 추가 
curl -H 'Content-Type: application/json' -X POST 'localhost:9200/boolean_sample/_doc/1' -d ' { "boolean_format":false }'

# 문자열로 boolean 타입의 데이터 추가 
curl -H 'Content-Type: application/json' -X POST 'localhost:9200/boolean_sample/_doc/2' -d ' { "boolean_format":"true" }'

# 데이터 조회 
curl -H 'Content-Type: application/json' -X GET 'localhost:9200/boolean_sample/_search?pretty' -d ' 
{ "query": { "term" : { "boolean_format": "true" } } }'

--- 반환값 ---
{
    ...
    "hits" : [
      {
        "_index" : "boolean_sample",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.6931471,
        "_source" : {
          "boolean_format" : "true"
        }
      }
    ]
  }
}

boolean 에 대한 자세한 내용은 여기 에서 확인할 수 있다.

 

5 객체(Object)

Elasticsearch 에서는 객체 타입의 값도 저장 가능하다. 객체 타입은 <이름> : <값> 형식의 데이터 포맷으로 이를 이용하면 필드 안에 또 다른 하위 필드로 객체 타입을 지정할 수 있다. 조회를 위한 데이터 접근은 <Object 이름>.<Object Filed 이름> 으로 접근하면 된다.  

# 자동 Mapping 으로 Object 타입이 설정되게 한다. 
curl -H 'Content-Type: application/json' -X POST 'localhost:9200/object_sample/_doc/1' -d ' 
{ "region": "US", "object_format": { "age": 20, "first": "John", "last": "Smith" } }'

# object_format 이 Object 타입으로 하위에 age, first, last 타입을 Mapping 된것을 확인한다. 
curl -H 'Content-Type: application/json' -X GET 'localhost:9200/object_sample/_mapping?pretty'

--- 반환값 ---
{
  "object_sample" : {
    "mappings" : {
      "properties" : {
        "object_format" : {
          "properties" : {
            "age" : { "type" : "long" },
            "first" : {
              "type" : "text",
              "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } }
            },
            "last" : {
              "type" : "text",
              "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } }
            }
          }
        },
        "region" : {
          "type" : "text",
          "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } }
        }
      }
    }
  }
}

# Object 타입 정보를 기준으로 조회
curl -H 'Content-Type: application/json' -X GET 'localhost:9200/object_sample/_search?pretty' -d ' 
{ "query": { "term" : { "object_format.first": "john" } } }'

--- 반환값 ---
{
    ...
    "hits" : [
      {
        "_index" : "object_sample",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.2876821,
        "_source" : {
          "region" : "US",
          "object_format" : {
            "age" : 20,
            "first" : "John",
            "last" : "Smith"
          }
        }
      }
    ]
  }
}

Object 에 대한 자세한 내용은 여기 에서 확인할 수 있다. 

 

6 중첩(Nested)

 배열과 같은 중첩 필드도 가능하다. Nested 타입은 트리 형태가 아닌 각각의 독립 데이터로 유지하기 위해 데이터를 펴서(flattened) 저장한다 때문에 서로 다른 데이터를 엮어서 검색이 가능하다.

예를 들어 다음과 같이 이름이 John Smith 와 Alice White 인 사용자를  저장했을 경우

# User 에 2개의 Object 를 Nested 하게 추가했다. 
curl -H 'Content-Type: application/json' -X POST 'localhost:9200/nested_sample/_doc/1' -d '
{
  "group" : "fans",
  "user" : [ { "first" : "John", "last" :  "Smith"},
             { "first" : "Alice", "last" :  "White" } ]
}'

기대는 Object 타입의 데이터 2개가 독립적으로 형성되었길 기대했지만 실제 내부적으로는 다음과 같은 형태로 저장되어 있다. 

{ 
  "group": "fans"
  "user.first": ["John", "Alice"]
  "user.last": ["Smith", "White"]
}

# Mapping 상태 조회 
curl -H 'Content-Type: application/json' -X GET 'localhost:9200/nested_sample/_mapping?pretty'
{
  "nested_sample" : {
    "mappings" : {
      "properties" : {
        "group" : {
          "type" : "text",
          "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } }
        },
        "user" : {
          "properties" : {
            "first" : {
              "type" : "text",
              "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } }
            },
            "last" : {
              "type" : "text",
              "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } }
            }
          }
        }
      }
    }
  }
}

Mapping 상태를 조회했을 때도 Nested 타입으로 Mapping 되었다는 정보는 어디에도 없다. 

때문에 현재 상태에서는 다음과 같이 서로 다른 하위 데이터를 섞어서도 검색이 가능하다. 

curl -H 'Content-Type: application/json' -X GET 'localhost:9200/nested_sample/_search?pretty' -d '
{ "query":
  { "bool" :
    { "must": [
      { "match" : { "user.first":"John" } },
      { "match" : { "user.last": "White" } }
    ]}
  }
}'

--- 반환값 ---
{
    ...
    "hits" : [
      {
        "_index" : "nested_sample",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.5753642,
        "_source" : {
          "group" : "fans",
          "user" : [
            { "first" : "John", "last" : "Smith" },
            { "first" : "Alice", "last" : "White" }
          ]
        }
      }
    ]
  }
}

하지만 John White 라는 사용자가 있는지를 찾고자 했다면 기대했던 결과는 아닐 것이다.

이렇게 중첩된 Object 를 하나의 독립적인 단위로 조회하고 싶을 때는 Nested Query 를 사용해야 한다. 

단 Nested Query 를 사용하기 위해서는 독립적인 단위가 될 Object 필드가 Nested 타입으로 지정해야 한다.

# 위에서 사용했던 Index 는 삭제한다. 
curl -H 'Content-Type: application/json' -X DELETE 'localhost:9200/nested_sample?pretty'

# 신규 Index 를 생성하면서 user 를 nested 타입으로 지정한다. 
curl -H 'Content-Type: application/json' -X PUT 'localhost:9200/nested_sample?pretty' -d '
{
  "mappings": {
    "properties": {
      "user": {
        "type": "nested" 
      }
    }
  }
}'

# 아까와 같이 2개의 user Object 를 Nested 하게 추가한다. 
curl -H 'Content-Type: application/json' -X POST 'localhost:9200/nested_sample/_doc/1' -d '
{
  "group" : "fans",
  "user" : [ { "first" : "John", "last" :  "Smith"},
             { "first" : "Alice", "last" :  "White" } ]
}'

# Nested Query 를 이용해서 조회한다. 
curl -H 'Content-Type: application/json' -X GET 'localhost:9200/nested_sample/_search?pretty' -d ' 
{
  "query": {
    "nested": {
      "path": "user",
      "query": {
        "bool": {
          "must": [
            { "match": { "user.first": "John" }},
            { "match": { "user.last":  "White" }} 
          ]
        }
      }
    }
  }
}'

--- 반환값 ---
{
    ...
    "hits" : [ ]
  }
}


# John Smith 로 조회해본다. 
curl -H 'Content-Type: application/json' -X GET 'localhost:9200/nested_sample/_search?pretty' -d ' 
{
  "query": {
    "nested": {
      "path": "user",
      "query": {
        "bool": {
          "must": [
            { "match": { "user.first": "John" }},
            { "match": { "user.last":  "Smith" }} 
          ]
        }
      }
    }
  }
}'
--- 반환값 ---
{
    ...
    "hits" : [
      {
        "_index" : "nested_sample",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.3862942,
        "_source" : {
          "group" : "fans",
          "user" : [ { "first" : "John", "last" : "Smith" },
                     { "first" : "Alice", "last" : "White" } ]
        }
      }
    ]
  }
}

 

기대대로 John White 라는 사용자로는 조회되지 않고 John Smith 로는 조회가 된다.

Mapping 상태도 이전과는 다르게 user 가 nested 타입으로 지정되었다. 

curl -H 'Content-Type: application/json' -X GET 'localhost:9200/nested_sample/_mapping?pretty'

--- 반환값 ---
{
  "nested_sample" : {
    "mappings" : {
      "properties" : {
        "group" : {
          "type" : "text",
          "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } }
        },
        "user" : {
          "type" : "nested",
          "properties" : {
            "first" : {
              "type" : "text",
              "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } }
            },
            "last" : {
              "type" : "text",
              "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } }
            }
          }
        }
      }
    }
  }
}

Nested 데이터 타입에 대한 자세한 내용은 여기 에서 확인할 수 있다. 

Nested Query 에 대한 내용은 여기 에서 확인할 수 있다. 

 

오늘은 여기까지~

누군가에게 도움이 되었길 바라면서 오늘의 포스팅 끝~

댓글