본문 바로가기
📂 데이터베이스/◾ ELASTICSEARCH

[ELK Stack] Elasticsearch의 이해, 매핑(mapping)

by 이 정규 2023. 6. 23.
728x90
반응형

Elasticsearch 공부해서 회사에서 살아남기 (2)

지난 포스팅에서는 REST API와 CRUD에 대해서 알아봤다. 지난 포스팅을 보지 않았다면 아래 링크에서 한 번 훑고 이번 포스팅 내용을 보는 것을 추천한다.

2023.06.19 - [데이터베이스] - [ELK Stack] Elasticsearch의 이해, REST API와 CRUD

 

[ELK Stack] Elasticsearch의 이해, REST API와 CRUD

Elasticsearch 공부해서 회사에서 살아남기 (1) ELK Stack 다니는 직장에서는 ELK(Elasticsearch + Logstash + Kibana) Stack을 사용한다. 물론 RDBMS도 사용하지만 각각의 용도가 다르다. ELK Stack은 쿼리 - 수집 - 시각

zzgrworkspace.tistory.com

mapping

매핑은 JSON 형태의 데이터를 루씬이 이해할 수 있도록 바꿔주는 작업이다. 엘라스틱 서치가 자동으로 하면 다이나믹 매핑(dynamic mapping), 사용자가 직접 설정하면 명시적 매핑(explicit mapping)이다. 매핑 설정 방법과 좋은 매핑이 무엇인지 알아보도록 하자.지난 포스팅에서 사용했던 index2을 예시로 매핑이 뭔지 간단히 보고 넘어가보자.

PUT index2/_doc/1
{
  "name" : "Evan",
  "age" : 28,
  "addr" : "Seoul"
}

PUT index2/_doc/2
{
  "name" : "Jane",
  "country" : "France"
}

PUT index2/_doc/3
{
  "name" : "Alan",
  "age" : "33",
  "gender" : "male"
}

PUT index2/_doc/1
{
  "name" : "Park",
  "age" : 45,
  "gender" : "male"
}

POST index2/_update/1
{
  "doc" :{
    "name" : "Lee"
  }
}

뭘 많이 만지작거렸지만 결과적으로 도큐먼트가 3개 있고, 각 도큐먼트마다 필드가 2-3개씩 있다. 매핑은 딱히 손대지 않았으니 다이나믹 매핑이 됐을거다. GET 메서드로 매핑이 어떻게 되어있는지 확인해보자.

GET index2/_mapping
{
  "index2": {
    "mappings": {
      "_doc": {
        "properties": {
          "addr": {
            "type": "text"
          },
          "age": {
            "type": "long"
          },
          "country": {
            "type": "text"
          },
          "gender": {
            "type": "text"
          },
          "name": {
            "type": "text"
          }
        }
      }
    }
  }
}

GET 메서드로 mapping이 어떻게 되어있는지 확인해봤다. 그 중 `long`은 int 형 값을 보고 자동으로 지정해준 필드이다. 그런데 이를 명시적 매핑을 통해 `short`로 수정해주는게 효율적이다. 이유는 아래 엘라스틱 서치 공식 문서에 나와있는 숫자 필드에 대한 내용을 보면 이해가 될거다.

< numeric filed >
long : 64비트 정수 (-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807)
integer : 32비트 정수 (-2147483648 ~ 2147483647)
short : 16비트 정수 (-32768 ~ 32767)
byte : 8비트 정수 (-128 ~ 127)
double : 64비트 실수
float : 32비트 실수
half_float : 16비트 실수
scaled_float : 실수형이지만 부동소수점이 아니라 long 형태로 저장하고 옵션으로 소수점 위치를 지정합니다. 
통화 (예: $19.99) 같이 소수점 자리가 고정된 값을 표시할 때 유용합니다.

이번에는 새로운 인덱스 index3을 만든다.  "age"는 `short`, "name"은 `text`, "gender"는 `keyword`로 매핑해준다.

PUT index3
{
  "mappings": {
    "properties": {
      "age" : {"type": "short"},
      "name" : {"type": "text"},
      "gender" : {"type": "keyword"}
    }
  }
}
GET index3/_mapping

명시적 매핑을 통해 입력한 필드가 잘 출력되는 것을 확인할 수 있다.

엘라스틱 서치의 문자열 필드에는 `text`와 `keyword` 두가지 뿐이다. 두 가지의 차이는 애널라이저를 적용하느냐 안하느냐의 차이다. 그리고 매핑에 필드를 미리 정의하지 않으면 동적 문자열 필드가 생성 될 때 `text` 필드와 `keyword` 필드가 다중 필드로 같이 생성된다. 아래 공식 문서의 문자열 필드에 대한 설명이다. 참고하면 좋을 것 같다.

< string field >
text
text 타입은 입력된 문자열을 텀 단위로 쪼개어 역 색인 (inverted index) 구조를 만듭니다. 
보통은 풀텍스트 검색에 사용할 문자열 필드 들을 text 타입으로 지정합니다.
text 필드에 설정 가능한 옵션들은 다음과 같은 것들이 있습니다.

keyword
keyword 타입은 입력된 문자열을 하나의 토큰으로 저장합니다. 
text 타입에 keyword 애널라이저를 적용 한 것과 동일합니다. 
보통은 집계(aggregation) 또는 정렬(sorting)에 사용할 문자열 필드를 keyword 타입으로 지정합니다. 
keyword 필드에 설정 가능한 옵션들은 다음과 같은 것들이 있습니다.

도큐먼트에는 원래 하나의 필드값만 있지만 필드의 값을 여러 개의 역 색인 및 도큐먼트 값들로 저장할 수 있는 것이 다중 필드이다. 보통은 `text` 타입 아래에 `keyword` 타입을 같이 정의하기 위해서 사용된다. 한 필드에 여러 애널라이저를 적용해야 하는 경우나 다국어로 씌여진 도큐먼트를 분석해야 할 때 유용하다.

멀티 필드를 활용한 문자열 처리를 해보겠다. 그 전에 애널라이저가 어떻게 결과를 출력하는지 확인해보자.

POST _analyze
{
  "analyzer": "standard",
  "text": "Every company is becoming an AI company and engineers are on the front lines\
  of helping their organizations make this transition. In order to enhance their products,\
  engineering teams are increasingly being asked to incorporate machine learning \
  into their product roadmaps and monthly OKRs. This can be anything from implementing\
  personalized experiences and fraud detection systems to most recently,\
  natural language interfaces powered by large language models."
}
{
  "tokens": [
    {
      "token": "every",
      "start_offset": 0,
      "end_offset": 5,
      "type": "<ALPHANUM>",
      "position": 0
    },
    {
      "token": "company",
      "start_offset": 6,
      "end_offset": 13,
      "type": "<ALPHANUM>",
      "position": 1
    },
    {
      "token": "is",
      "start_offset": 14,
      "end_offset": 16,
      "type": "<ALPHANUM>",
      "position": 2
    },
    {
      "token": "becoming",
      "start_offset": 17,
      "end_offset": 25,
      "type": "<ALPHANUM>",
      "position": 3
    },
    {
      "token": "an",
      "start_offset": 26,
      "end_offset": 28,
      "type": "<ALPHANUM>",
      "position": 4
    },
    
    ...

아래 출력 결과와 같이 분석(analyze)은 주어진 텍스트를 토큰(token) 단위로 분할하고, 각 토큰의 형태소 분석(morphological analysis) 및 정규화(normalization) 등의 처리를 수행하는 과정이다. 이를 통해 텍스트를 색인하기 전에 적절한 형태로 처리할 수 있다.

위의 코드에서는 "standard" 분석기를 사용하여 주어진 텍스트를 분석한다. 스탠다드 분석기는 기본적인 텍스트 분석을 수행하며, 주어진 텍스트를 공백을 기준으로 토큰화하고 소문자로 변환하는 등의 처리를 수행한다.

PUT multifiled_index
{
  "mappings": {
    "properties": {
      "msg": {"type": "text"},
      "contents": {
        "type": "text",
        "fields": {
          "keyword": {"type": "keyword"}
        }
      }
    }
  }
}

인덱스를 생성하고 매핑을 정의해준다. "msg" 필드와 "contents" 필드를 정의해주는데, "msg" 필드는 "text" 데이터 타입으로 지정했고, "contents" 필드는 "text" 데이터 타입으로 지정했으며, 추가로 "keyword" 데이터 타입의 서브 필드를 가지도록 설정했다.

PUT multifiled_index/_doc/1
{
  "msg": "1 document",
  "contents": "beautiful day"
}

PUT multifiled_index/_doc/2
{
  "msg": "2 document",
  "contents": "beautiful day"
}

PUT multifiled_index/_doc/3
{
  "msg": "3 document",
  "contents": "wonderful day"
}

 "multifiled_index" 인덱스에 문서를 색인(indexing)해준다. 문서를 인덱스에 저장하여 검색하고 조회하는 작업을 수행할 수 있다.

위의 코드에서는 "multifiled_index" 인덱스에 3개의 문서를 색인하고 있고, 각 문서는 "_doc" 타입으로 지정되며, 문서 내부에는 "msg" 필드와 "contents" 필드가 포함되어 있다.

인덱스 전문 쿼리와 용어 쿼리와 실행 결과를 살펴보자.

GET multifiled_index/_search
{
  "query": {
    "match": {
      "contents": "day"
    }
  }
}
{
  "hits": {
    "total": {
      "value": 3,
      "relation": "eq"
    },
    "hits": [
      {
        "_index": "multifiled_index",
        "_type": "_doc",
        "_id": "1",
        "_score": 0.6931471,
        "_source": {
          "msg": "1 document",
          "contents": "beautiful day"
        }
      },
      {
        "_index": "multifiled_index",
        "_type": "_doc",
        "_id": "2",
        "_score": 0.6931471,
        "_source": {
          "msg": "2 document",
          "contents": "beautiful day"
        }
      },
      {
        "_index": "multifiled_index",
        "_type": "_doc",
        "_id": "3",
        "_score": 0.2876821,
        "_source": {
          "msg": "3 document",
          "contents": "wonderful day"
        }
      }
    ]
  }
}

검색 결과는 "beautiful day"를 포함하는 두 개의 도큐먼트와 "wonderful day"를 포함하는 하나의 도큐먼트가 반환된다. 각 문서에 대한 일치도 점수(_score)도 표시된다. 점수가 높을수록 관련도가 높은 도큐먼트라고 판단해서 상위로 노출시킨다.

GET multifiled_index/_search
{
  "query": {
    "term": {
        "contents.keyword": "day"
    }
  }
}
{
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "hits": [
      {
        "_index": "multifiled_index",
        "_type": "_doc",
        "_id": "1",
        "_score": 0.0,
        "_source": {
          "msg": "1 document",
          "contents": "beautiful day"
        }
      },
      {
        "_index": "multifiled_index",
        "_type": "_doc",
        "_id": "2",
        "_score": 0.0,
        "_source": {
          "msg": "2 document",
          "contents": "beautiful day"
        }
      }
    ]
  }
}

검색 결과에는 "beautiful day"를 포함하는 두 개의 도큐먼트가 반환된다. 각 문서의 점수는 0으로 표시된다. `term` 쿼리는 정확한 일치를 기반으로 하기 때문에 유사성 점수를 계산하지 않는다. 따라서 모든 문서의 점수가 0으로 설정된다.

"wonderful day" 도큐먼트의 경우 "contents.keyword" 필드 값이 "wonderful day"이므로 "day"와 정확히 일치하지 않아 검색 결과에 포함되지 않았다.

결론적으로, 첫 번째 쿼리는 "day"와 텍스트 필드에서 부분 일치하는 문서를 반환하는 반면, 두 번째 쿼리는 "day"와 `keyword` 타입의 필드에서 정확히 일치하는 문서를 반환한다.

그렇다면 "wonderful day"를 `term` 쿼리로 검색해보자.

GET multifiled_index/_search
{
  "query": {
    "term": {
        "contents.keyword": "wonderful day"
    }
  }
}
{
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "hits": [
      {
        "_index": "multifiled_index",
        "_type": "_doc",
        "_id": "3",
        "_score": 0.0,
        "_source": {
          "msg": "3 document",
          "contents": "wonderful day"
        }
      }
    ]
  }
}

검색 결과에는 "wonderful day"를 정확히 포함하는 하나의 도큐먼트가 반환된다. 

`term` 쿼리는 정확한 일치를 요구하므로, 다른 문서들은 검색어와 정확히 일치하지 않기 때문에 결과에 포함되지 않는다.

마지막으로 인덱스 집계 쿼리 실행 결과를 살펴보자.

GET multifiled_index/_search
{
  "size": 0,
  "aggs": {
    "contents": {
      "terms": {
        "field": "contents.keyword"
      }
    }
  }
}
{
  "aggregations": {
    "contents": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        {
          "key": "beautiful day",
          "doc_count": 2
        },
        {
          "key": "wonderful day",
          "doc_count": 1
        }
      ]
    }
  },
  "hits": {
    "total": {
      "value": 3,
      "relation": "eq"
    },
    "max_score": null,
    "hits": []
  }
}

검색 결과에는 실제 검색된 문서는 없지만, 집계(aggregation) 결과가 반환된다. 집계는 검색 결과를 대상으로 특정 필드의 값들을 집계하여 통계 정보를 제공하는 기능이다.

이 쿼리에서는 "contents.keyword" 필드를 기준으로 집계를 수행하고 있다. 결과로는 두 개의 버킷(bucket)이 반환된다.

각 버킷의 "doc_count"는 해당 값을 가진 문서의 개수를 나타내고 있다. 이를 통해 "contents.keyword" 필드의 값들에 대한 집계 결과를 확인할 수 있다.

"size" 매개변수를 0으로 설정하여 실제 검색 결과는 표시되지 않으며, 집계 결과만 반환된다.

원래 템플릿까지 정리해보려 했는데, 쓰다보니 너무 길어져서 매핑만 정리해보았다. 다음 포스팅에서는 템플릿에 관련된 내용을 다뤄볼 예정이다. 사실 매핑도 조금 더 살을 붙이고 싶다만 내 비루한 필력과 정리 능력으로는 이 정도로 만족해야 할 것 같다. 오늘도 남의 공부 봐주셔서 감사드리고 좋은 주말 보내시길 바랍니다!

728x90
반응형

댓글