반응형

엘라스틱서치에서 데이터를 대소문자 구분없이 검색하기 위해서는 lowercase 혹은 uppercase 토큰 필터를 적용해야한다. 토큰필터가 적용되게 색인하기 위해서는 인덱스 매핑 시점에 필드에 analyzer나 normalizer를 설정하면 된다.

Elasticsearh 공식문서, 김종민님의 공식가이드북, 책 엘라스틱서치 실무 가이드를 참고하여 제가 이해한 내용을 정리한 포스팅입니다. 포스팅 내용의 오류를 발견하시면 댓글 부탁드립니다 :)

예를 들어 cafe, Cafe, café 는 모두 다른 term이다. 하지만 사용자가 "cafe"로 검색했을 때 대소문자 구분없이 세 개의 term이 모두 검색되도록 할 수 있다. (café는 아스키코드를 처리해야하는 데 아래에서 내용을 다룬다)

필드에 토큰 필터를 적용하면 대소문자 구분없이 색인 및 검색이 되게 할 수 있다. 아래 두 가지 방법으로 인덱스 생성 시에 토큰 필터를 적용할 수 있다.

  1. text 타입에 analyzer를 적용
  2. keyword 타입에 normalizer를 적용

본 포스팅에서는 대소문자 구분없이 검색이 가능하도록 인덱스를 색인하고, 각 매핑 방법에 따라 쿼리 질의와 결과가 어떻게 달라지는 지 확인해보겠다.


그 전에 엘라스틱서치 간단 용어 설명을 하겠다.

용어는 위에서 언급한 책을 기준으로 설명하였으며, 정확한 최신 정보를 원한다면 엘라스틱 공식문서와 공식가이드북을 확인하도록 하자.

필드 타입 (공식가이드북 확인)

인덱스 생성 시 필드에 정의 가능한 타입을 뜻하는 것으로 text, keyword, date, integer, boolean 등이 있다.

  • text
    1. 색인 시 지정된 분석기가 컬럼의 데이터를 문자열 데이터로 인식하고 이를 분석함
    2. 문장 형태의 데이터에 적합한 타입임
    3. 전문 검색 가능
    4. 정렬이나 집계(Aggregation) 사용 X
    5. 정렬이나 집계 연산을 사용해야할 때, Text 타입과 Keyword 타입을 동시에 갖도록 멀티 필드로 설정하여 이용할 수 있음
  • keywrod
    1. 키워드 형태로 사용할 데이터에 적합한 데이터 타입
    2. 별도의 분석기를 거치지 않고 원문 그대로 색인
    3. 정형화된 콘텐츠에 주로 사용됨
    4. 일부 기능은 형태소 분석을 하지 않아야만 사용 가능한데, 이 경우에도 Keyword 데이터 타입을 사용함
    5. keyword 타입을 사용해야하는 경우 : 집계(Aggregation) 사용, 정렬, 검색 시 필터링

매핑 파라미터 (공식문서 확인)

매핑 파라미터는 색인할 필드의 데이터를 어떻게 저장할지에 대한 다양한 옵션을 뜻함

  • analyzer
    1. 해당 필드를 형태소 분석하겠다는 의미
    2. 색인과 검색 시 지정한 분석기로 형태소 분석을 수행
    3. text 데이터 타입의 필드는 analyzer 매핑 파라미터를 기본적으로 사용해야 함
    4. 별도의 분석기 미지정 시 Standard Analyzer로 형태소 분석
  • normalizer
    1. term query 분석기를 사용하기 위해 쓰임
    2. keyword 데이터 타입의 경우 원문을 기준으로 문서가 색인되기 때문에 cafe, Cafe, café는 서로 다른 문서로 인식됨
    3. 하지만 해당 유형을 normalizer를 통해 분석, lowercase, asciifolding과 같은 토큰필터를 사용하면 같은 데이터로 인식 가능
    4. analyzer과 유사하나 tokenizer 적용이 불가하며, 캐릭터필터와 일부 토큰필터 사용 가능
    5. 사용 가능한 필터는 공식문서에서 확인할 수 있다 ex) lowercasing filter 적용 가능하지만, stemming filter는 불가함 (버전 7.6 기준)

실습해보기

 

  1. 인덱스 생성

테스트를 위해 keyword 타입의 필드에 normalizer를 적용하고, text 타입의 필드에 analyzer를 적용한다.

엘라스틱서치 버전은 책과 동일하게 6.4.3 버전으로 맞추었다. 문법이 크게 달라지지는 않았으므로 최신 버전에서도 무리없이 동작하리라 기대한다.

PUT text_and_keyword
{
  "settings": {
    "analysis": {
      "normalizer": {
        "my_normalizer": {
          "type": "custom",
          "filter": ["lowercase", "asciifolding"]
        }
      },
      "analyzer": {
        "my_analyzer": {
          "type": "custom",
          "tokenizer": "keyword",
          "filter": ["lowercase", "asciifolding"]
        }
      }
    }
  },
  "mappings": {
    "_doc":{
      "properties": {
        "keyword_normalizer": {
          "type": "keyword",
          "normalizer": "my_normalizer"
        },
        "text_keyword_tokenizer": {
          "type": "text",
          "analyzer": "my_analyzer"
        }
      }
    }
  }
}
  • keyword 타입의 필드명 : keyword_normalizer
  • text 타입의 필드명 : text_keyword_tokenizer
  • lowercase token filter : term을 소문자로 변환
  • asciifolding token filter : 숫자, 영문자 아스키코드 외 문자를 아스키코드로 변환함

참고 : _doc 은 제외해도 된다. 엘라스틱서치 버전 6.0 이후부터 하나의 인덱스에 하나의 타입만 구성 가능하다. 

 

  1. 샘플 데이터 생성

테스트를 위해 샘플 문서를 색인한다.

POST /text_and_keyword/_doc/1
{
  "keyword_normalizer": "cafe",
  "text_keyword_tokenizer": "cafe"
}


POST /text_and_keyword/_doc/2
{
  "keyword_normalizer": "Cafe",
  "text_keyword_tokenizer": "Cafe"
}


POST /text_and_keyword/_doc/3
{
  "keyword_normalizer": "café",
  "text_keyword_tokenizer": "café"
}

[cafe, Cafe, café]는 역인덱스되어 모두 "cafe"라는 하나의 term으로 저장된다.

lowercase 토큰 필터를 통해 대문자는 모두 소문자로 변환되고, asciifolding 토큰 필터를 통해 é와 같은 문자를 아스키코드 내에 있는 영문자 e로 변환되어 색인된다.

 

  1. Term 쿼리

term 쿼리는 별도의 분석 작업을 수행하지 않고 입력된 텍스트가 존재하는 문서를 찾는다.

각 필드에 term 쿼리로 "CAFE"를 질의해본다.

POST text_and_keyword/_search
{
  "query": {
    "term": {
      "keyword_normalizer": "CAFE"
    }
  }
}

POST text_and_keyword/_search
{
  "query": {
    "term": {
      "text_keyword_tokenizer": "CAFE"
    }
  }
}

term 쿼리 결과는 각각 아래와 같다.

  • "keyword_normalizer" : normalizer가 적용된 keyword 타입의 필드

    ...
    "hits" : {
        "total": 3,
      "max_score": 0.2876821,
      "hits": [
              ... 3개의 문서 모두 표시 ...
      ]
    }
    ...

3개의 문서 cafe, Cafe, café가 모두 출력되었다.

term 쿼리는 검색 시 분석기가 적용되지 않지만, normalizer는 적용된다.

term 쿼리 검색 시 검색어 "CAFE"가 normalizer가 적용되어 소문자 "cafe"로 변환된다.

검색어 "cafe"로 색인된 "cafe"를 조회하기 때문에 3 개의 문서가 모두 검색되는 것이다.

  • "text_keyword_tokenizer" : analyzer - keyword tokenizer가 적용된 text 타입의 필드

    ...
    "hits": {
        "total": 0,
        "max_score": null,
        "hits": []
      }
    ...

term 쿼리는 별도의 분석 작업을 수행하지 않고 입력된 검색어가 존재하는 문서를 찾는다.

검색어를 하나의 term으로 취급하기 때문에 검색어와 정확히 일치하는 term이 존재하지 않는 경우 검색되지 않는다.

검색어 "CAFE"와 일치하는 term은 존재하지 않으므로 total 값이 0으로 나왔다.

 

  1. Match 쿼리 조회

match 쿼리는 검색어를 형태소 분석을 통해 term으로 분리 후, 이를 이용해 검색 질의를 수행한다.

각 필드에 match 쿼리로 질의해보자.

POST text_and_keyword/_search
{
  "query": {
    "match": {
      "keyword_normalizer": "CAFE"
    }
  }
}

POST text_and_keyword/_search
{
  "query": {
    "match": {
      "text_keyword_tokenizer": "CAFE"
    }
  }
}

match 쿼리 결과는 아래와 같다.

  • "keyword_normalizer" : normalizer가 적용된 keyword 타입의 필드

    ...
      "hits" : {
    	  "total": 3,
        "max_score": 0.2876821,
        "hits": [
    			... 3개의 문서 모두 표시 ...
        ]
      }
    ...

3개의 문서 cafe, Cafe, café가 모두 출력되었다.

  • "text_keyword_tokenizer" : analyzer - keyword tokenizer가 적용된 text 타입의 필드

    ...
      "hits" : {
    	  "total": 3,
        "max_score": 0.2876821,
        "hits": [
    			... 3개의 문서 모두 표시 ...
        ]
      }
    ...

3개의 문서 cafe, Cafe, café가 모두 출력되었다.

match 쿼리는 analyzer와 nomarlizer 모두 적용되므로 결과값에 차이가 없다.

하지만 질의 방식에 따라 엘라스틱서치 내부 검색과정이나 성능이 달라질 수 있어 가능한 용도에 맞게 사용하는 것이 좋다.

 

  1. 집계(Aggregation)

    POST text_and_keyword/_search?size=0
    {
      "aggs": {
        "my_keyword_aggr": {
          "terms": {
            "field": "keyword_normalizer"
          }
        }
      }
    }
    
    
    POST text_and_keyword/_search?size=0
    {
      "aggs": {
        "my_text_aggr": {
          "terms": {
            "field": "text_keyword_tokenizer"
          }
        }
      }
    }

집계 결과는 아래와 같다.

  • "keyword_normalizer" : normalizer가 적용된 keyword 타입의 필드

    ...
    "aggregations": {
        "my_keyword_aggr": {
          "doc_count_error_upper_bound": 0,
          "sum_other_doc_count": 0,
          "buckets": [
            {
              "key": "cafe",
              "doc_count": 3
            }
          ]
        }
      }
    ...

"cafe"라는 term으로 3개의 문서가 있음을 알 수 있다.

원본 데이터가 궁금하다면 쿼리 질의 시 size 파라미터를 제거하거나 늘리면 된다. (size의 기본값은 10이다)

  • "text_keyword_tokenizer" : analyzer - keyword tokenizer가 적용된 text 타입의 필드

...
"error": {
    "root_cause": [
      {
        "type": "illegal_argument_exception",
        "reason": "Fielddata is disabled on text fields by default. Set fielddata=true on [text_keyword_tokenizer] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead."
      }
...

오류가 발생한다.

text 타입은 집계(Aggregation)를 할 수 없다.


정리

대소문자 필터를 적용해서 색인된 결과는 같은데, 쿼리 질의 방법이 달라질 수 있다는 것은 다소 모호하게 느껴진다.

하지만 내가 검색해야할 데이터의 성격과 용도를 명확히 인지하고, text 타입과 keyword 타입의 특성을 이해한다면 선택은 비교적 간단해진다.

예를 들어, 집계나 정렬이 필요하고 형태소 분석이 필요가 없다면 keyword 타입에 normalizer 파라미터를 적용하면 된다. 하지만 형태서 분석이나 동의어 토큰 필터 등을 사용해야 한다면 text 타입에 analyzer를 사용하면 된다.

이 필드가 어떻게 검색되면 좋겠는 지에 따라 색인과 파라미터를 정의하면 되는 것이다.

반응형