시뻘건 개발 도전기

엘라스틱서치 api 본문

Reading/엘라스틱서치 실무 가이드

엘라스틱서치 api

시뻘건볼때기 2023. 12. 4. 11:54
반응형

api의 종류

앞서 계속 언급되었듯이 Elasticsearch는 RESTful 방식의 api를 제공하며 json 기반으로 통신한다. 아래와 같이 엘라스틱서치에서 api를 제공한다.

  • 인덱스 관리 api: 인덱스 관리
  • 문서 관리 api: 문서 추가/수정/삭제
  • 검색 api: 문서 조회
  • 집계 api: 문서 통계

문서를 색인하기 위해서는 기본적으로 인덱스를 생성해야한다. 인덱스를 통해 입력되는 문서의 필드를 정의하고 각 필드에 알맞는 데이터 타입을 지정한다. 이 과정을 통해 효율적으로 색인이 가능하다.

index vs indices
색인은 데이터가 토큰화되어 저장된 자료구조를 의미한다. 'index'를 번역하면 '색인'인데, elasticsearch에서 인덱스라는 영어를 색인과 다른 의미로 사용한다.

index: 색인 데이터
indexing: 색인하는 과정
indices: 맵핑 정보를 저장하는 논리적인 데이터 공간

elasticsearch에서는 용어 혼란을 방지하고자 색인을 의미할 경우 'index'라는 단어를 사용하고 매핑 정의 공간을 의미할 경우 'indices'라는 단어로 표현한다.

 

 

스키마리스

엘라스틱서치는 사용 편의성을 위해 스키마리스(schemaless)라는 기능을 제공한다. 인덱스를 생성하는 과정 없이 문서를 추가하더라도 문서가 색인되도록 하는 편의 기능이다. 스키마리스 기능은 최초 문서가 색인될 대 인덱스의 존재여부를 확인하는데, 존재하지 않는다면 문서를 분석해서 색인될 수 있게 인덱스를 자동으로 생성한다. 이 스키마리스 기능은 사용하지 않는 것을 권장한다.

스키마리스 기능은 가급적이면 사용하지 말자

 

스키마리스를 사용하면 다양한 비정형 데이터를 하나의 인덱스로 구성할 수 있다. 이는 성능과 밀접한 연관이 있기 때문에 특수한 상황에서만 사용해야한다. 그만큼 데이터 구조 및 검색 방식을 확실히 이해하고 사용해야한다.

테스트를 위해 인덱스를 생성하지 않고 바로 데이터를 색인해보자. 인덱스 맵핑 정보가 정의되지 않았기 때문에 json 형식의 name-value을 분석해서 필드명과 각종 속성정보를 자동으로 생성한다.

 

PUT /movie/_doc/1
{
    "movield": "1",
    "movieNn": "살아남은 아이",
    "movieNmEn": "Last Child",
    "prdtYear": "2017",
    "openDt": "",
    "typeNm": "장편",
    "prdtStatiNm": "기타"
    "naionAlt" : "한국" ,
    "genreAlt": "드라마,가족",
    "replationNn": "한국",
    "repsenreNn": "드라마"
}

# result
{
    "_index" : "movie",
    "_type" : "_doc",
    "_id" : "1",
    "_version" : 1,
    "result": "created",
    "_shards" : {
        "total" : 5,
        "successful" : 5,
        "failed" : 0
    },
    "_seg_no" : 0,
    " _primary_term" : 1
}

 

5개의 샤드로 구성된 movie 인덱스가 생성되었고 _id가 1인 문서가 추가되었다. 인덱스가 자동 생성되어 세부적인 필드 정보가 맵핑되지 않는 다는 문제점이 보인다. 이러면 특정 단어를 검색할 때 검색 결과에서 누락되는 등 문제가 발생할 가능성이 높아진다.

GET /movie

# result
{
    "movie": {
        "aliases": {},
        "mappings": {
            "_doc": {
                "properties": {
                    "genreAlt": {
                        "type": "text",
                        "fields": {
                            "keyword": {
                                "type": "keyword",
                                "ignore_above": 256
                            }
                        }
                    },
                    "movieCd": {
                        "type": "text",
                        "fields": {
                            "keyword": {
                                "type": "keyword",
                                "ignore_above": 256
                            }
                        }
                    },
                    "movieNm": {
                        "type": "text"
                        "fields": {
                            "keyword": {
                            "type": "keyword",
                            "ignore _above": 256
                            }
                        }
                    },

                    ( ・ ・ ・ 생략  ・ ・ ・)

                    "typeNm": {
                        "type": "text",
                        "fields": {
                            "keyword": {
                            "type": "keyword",
                            "ignore_above": 256
                            }
                        }
                    }
                }
            }
        },
        "settings": {
            "index": {
                "creation_date": "1545699886797",
                "number_of _shards": "5",
                "number _of_replicas": "1",
                "uuid": "wihohbsZRzuUvmXJYOX3Ag",
                "version": {
                    "created": "6040399"
                },
                "provided_name": "movie"
            }
        }
    }
}

 

모든 필드가 text 타입과 keyword 타입을 동시에 제공하는 멀티필드 기능으로 구성된다. 하지만 특정 필드는 하나의 타입만 필요할 수도 있다. 이런 경우에는 공간이 낭비된다. 실무에서는 복잡한 구조의 데이터를 다루기 때문에 자칫 잘못하면 검색 품질이 떨어지거나 성능상 문제가 발생할 가능성이 커질 수 있다.

아버지가 방에 들어 가신다

위와 같은 문장을 색인한다고 가정할 때, 스키마리스를 이용해 색인한다면 기본적으로 text type의 Standard Analyzer를 사용하는 데이터 타입이 정의될 것이다.

"아버지가", "방에", "들어", "가신다"

4가지의 토큰으로 분리되어 텀(Term)이 생성되고 검색 시 "아버지"라는 키워드가 입력되어도 해당 문서는 검색되지 않을 것이다. 검색을 위해서는 Standard Analyzer가 분리한 토큰 그대로 "아버지가"라는 키워드를 입력해야만 조회가 될 것이다. 이처럼 스키마리스 기능 사용을 지양하고 인덱스를 직접 정의해서 사용하는 것을 권장한다.

노드 설정 파일에서 action.auto_create_index를 false로 설정하면 인덱스가 자동으로 생성되지 않는다.
index.mapper.dynamic을 false로 설정하면 특정 컬럼의 자동 맵핑 생성을 비활성화한다.

 

 

인덱스 관리 api

인덱스를 추가하거나 삭제할 수 있다.

인덱스 생성

인덱스를 생설할 때는 맵핑이라는 세부 설정을 이용할 수 있는데 맵핑은 문서와 문서에 포함된 필드, 타입 등을 세세하게 지정하는 것이 가능한 설정 방식이다. 한 번 생성된 맵핑 정보는 변경할 수 없다. 아래와 같이 movie 인덱스를 생성하고 맵핑 정보를 추가해보자. 단순 문자열로 저장하고 싶을 경우 keyword 타입을 사용하고 형태소 분석을 원할 경우 text 타입을 사용한다.

PUT /movie
{
    "settings": {
        "number_of _shards": 3,
        "number_of_replicas": 2
    },
    "mappings": {
        "_doc": {
            "properties": {
                "movieCd": { "type" : "integer" },
                "movieNm": { "type" : "text" },
                "movieNmEn": { "type" : "text" },
                "prdtYear": { "type" : "integer" },
                "openDt": { "type" : "date" },
                "typeNm": { "type" : "keyword" },
                "protStatNm": { "type" : "keyword" },
                "nationAlt": { "type" : "keyword" },
                "genreAlt": { "type" : "keyword" },
                "repNationNm": { "type" : "keyword" },
                "repGenreNm": { "type" : "keyword" }
            }
        }
    }
}

# result
{
    "acknowledged": true,
    "shards_acknowledged": true,
    "index": "movie"
}

 

 

 

인덱스 삭제

위 생성한 movie 인덱스를 삭제해보자. 삭제는 한 번 실행하면 다시 복구할 수 없다.

DELETE /movie

# result
{
    "acknowledged": false
}

# 인덱스 이름이 잘못되었거나 없는 인덱스를 삭제할 때
{
    "error": {
        "root_cause": [
            {
                "type": "index_not_found_exception",
                "reason": "no such index",
                "index_uuid": "RO2VDL8809Wn0x14NXEnLA",
                "index": "movie"
            }
        ],
        "type": "index _not_found_exception",
        "reason": "no such index",
        "index_uuid": "RO2VDL8809WnQx14NXEnLA",
        "index": "movie"
    },
    "status": 404
}

 

문서 관리 api

문서를 색인하고 조회/수정/삭제를 지원한다. 엘라스틱서치는 검색엔진이기 때문에 다양한 검색 패턴을 지원하는 Search api를 별도로 제공한다. 하지만 색인된 ID를 기준으로 한 건의 문서를 다뤄야하는 경우 문서 관리 api를 사용한다. 한 건의 문서를 처리하기 위한 기능인 Single-document api는 아래와 같다.

  • Index api: 한 건의 문서를 색인한다.
  • Get api: 한 건의 문서를 조회한다.
  • Delete api: 한 건의 문서를 삭제한다.
  • Update api: 한 건의 문서를 업데이트한다.

실무에서는 클러스터를 운영하고 있기 때문에 다수의 문서를 처리해야하는 경우가 종종 발생한다. 이런 경우 Multi-document api를 사용한다.

  • Multi Get api: 다수의 문서를 조회한다.
  • Bulk api: 대량의 문서를 색인한다.
  • Delete By Query api: 다수의 문서를 삭제한다.
  • Update By Query api: 다수의 문서를 업데이트한다.
  • Reindex api: 인덱스의 문서를 다시 색인한다.

- 문서 생성 -

movie 인덱스에 문서를 추가해보자. 문서를 추가할 때 Id를 지정하지 않으면 uuid를 통해 자동으로 생성해준다. 이 기능은 편해보이지만 무작위로 생성된 id 때문에 문서를 업데이트할 때 애로사항이 생긴다. 예를들어 엘라스틱서치와 동기화된 데이터베이스의 데이터가 변경되었다고 가정해보자. 동기화가 되기 위해서는 엘라스틱서치에 색인된 _id값을 데이터베이스의 PK(기본키) 혹은 식별이 되는 키 값과 매칭한 정보가 어딘가에는 저장되어 관리되어야 한다. 하지만 대용량의 데이터에 해당 식별 값을 어딘가에 별도로 저장하는 것은 거의 불가능하다. 그래서 색인된 문서의 _id 값은 업데이트를 고려해서 데이터베이스 테이블의 식별 값과 맞춰 주는 것이 중요하다.

POST /movie/_doc/1
{
    "movieCd": "1",
    "movieNn": "살아남은 아이",
    "movieNmEn": "Last Child",
    "prdtYear": "2017",
    "openDt": "2017-10-20",
    "typeNm": "장편",
    "prdtStatiNm": "기타",
    "naionAlt" : "한국" ,
    "genreAlt": "드라마,가족",
    "replationNn": "한국",
    "repsenreNn": "드라마"
}

# result
{
    "_index" : "movie",
    "_type" : "_doc",
    "_id" : "1",
    "_version" : 1,
    "result": "created",
    "_shards" : {
        "total" : 2,
        "successful" : 2,
        "failed" : 0
    },
    "_seg_no" : 0,
    " _primary_term" : 1
}

 

# Id를 지정하지않고 문서 생성
POST /movie/_doc
{
    "movieCd": "1",
    "movieNn": "살아남은 아이",
    "movieNmEn": "Last Child",
    "prdtYear": "2017",
    "openDt": "2017-10-20",
    "typeNm": "장편",
    "prdtStatiNm": "기타",
    "naionAlt" : "한국" ,
    "genreAlt": "드라마,가족",
    "replationNn": "한국",
    "repsenreNn": "드라마"
}

# result
{
    "_index" : "movie",
    "_type" : "_doc",
    "_id" : "HZuX6mEB06UMLLexnak",
    "_version" : 1,
    "result": "created",
    "_shards" : {
        "total" : 2,
        "successful" : 2,
        "failed" : 0
    },
    "_seg_no" : 0,
    " _primary_term" : 1
}

 

- 문서 조회 -

GET /movie/_doc/1

# result
{
    "_index": "movie",
    "_type": "_doc",
    "_id": 1,
    "_version": 1,
    "found": true,
    "source": {
        "movield": "1",
        "movieNn": "살아남은 아이",
        "movieNmEn": "Last Child",
        "prdtYear": "2017",
        "openDt": "2017-10-20",
        "typeNm": "장편",
        "prdtStatiNm": "기타",
        "naionAlt" : "한국" ,
        "genreAlt": "드라마,가족",
        "replationNn": "한국",
        "repsenreNn": "드라마"
    }
}

 

- 문서 삭제 -

DELETE /movie/_doc/1

# result
{
    "_index": "movie",
    "_type": "_doc",
    "_id": 1,
    "_version": 1,
    "result": "delete",
    "_shards": {
        "total": 2,
        "successful": 2,
        "failed": 0
    }
}

 

검색 api

검색 api의 사용방식은 크게 두 가지로 나뉜다.

  • HTTP URI 형태의 파라미터를 URI에 추가해 검색하는 방법
  • RESTful api 방식인 QueryDSL을 사용해 요청 본문에 질의 내용을 추가해 검색하는 방법

현업에서는 Request Body 방식을 선호한다. URI방식보다 제약사항이 적기 때문이다. 간단한 표현식이라면 두 가지 형식을 섞어서 사용하는 것도 가능하다. 예를들어 query를 URI 방식으로 사용하고 나머지 기능을 json 타입으로 사용한다. QueryDSL을 사용하면 가독성이 높고 json 형식으로 다양한 표현이 가능해진다. json 포맷 헤더에는 쿼리가 실행된 총 시간(time_out)과 결과를 보여준다. hits에서는 일치하는 문서의 수와 함께 점수(_score)가 가장 높은 상위 10개의 문서를 보여준다. 검색의 실패한 샤드의 수는 검색시 설정된 time_out에 따라 결정된다. time_out이 초과되면 그 때까지 검색된 내용만 리턴된다. 따라서 time_out 시간을 적절하게 조정해야할 수도있다.

 

GET /movie/_doc/_search?q=prdtYear:2017&pretty=true
{
    "sort" : {
        "movieCd" : {
            "order" : "asc"
        }
    }
}

# URI 방식의 검색 질의
GET /movie/_doc/HZuX6mEB06UMLL9exnak?pretty=true

# result
{
    "_index": "movie",
    "_type": "_doc",
    "_id": "HZuX6mEB06UMLL9exnak",
    "_version": 4,
    "found": true,
    "source": {
        "movieCd": "1",
        "movieNn": "살아남은 아이",
        "movieNmEn": "Last Child",
        "prdtYear": "2017",
        "openDt": "2017-10-20",
        "typeNm": "장편",
        "prdtStatiNm": "기타",
        "naionAlt" : "한국" ,
        "genreAlt": "드라마,가족",
        "replationNn": "한국",
        "repsenreNn": "드라마"
    }
}

# 키 값과 일치하는 문서가 없을 때
{
    "_index": "movie",
    "_type": "_doc",
    "_id": "1",
    "found": false
}

# q 파라미터를 사용해 용어와 일치하는 문서 조회
POST /movie/_search?q=장편

# result
{
    "took": 1403,
    "timed_out": false,
    "_shards": {
        "total": 3,
        "successful": 3,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 1,
        "max_score": 0.18232156,
        "hits": [
            {
                "_index": "movie",
                "_type": "_doc",
                "id": "HZuX6mEB06UMLL9exnak",
                "_score": 0.18232156,
                "source": {
                    "movieCd": "1",
                    "movieNm":"살아남은 아이",
                    "movieNmEn": "Last Child",
                    "prdtYear": "2017",
                    "openDt": "2017-10-20",
                    "typeNm":"장편",
                    "prdtStatNm":"기타",
                    "nationAlt":"한국",
                    "genreAlt":"드라마,가족",
                    "repNationNm": "한국",
                    "repGenreNm":"드라마"
                }
            }
        ]
    }
}

 

q 파라미터를 사용할 때 별도의 필드명을 지정하지 않으면 존재하는 모든 필드를 대상으로 검색을 수행한다. 특정 필드만 조회하고 싶다면 아래 코드와 같이 필드명을 포함시키면 된다.

POST /movie/_search?q=typeNm:장편

# result
{
    (... 생략...)
    
    "hits": [
        {
            "_index": "movie",
            "_type": "_doc",
            "_id": "HZuX6mEBOGUMLLexnak",
            "_score": 0.18232156,
            " source": {
                "movieCd": "1",
                "movieNm":"살아남은 아이",
                "movieNmEn": "Last Child",
                "prdtYear": "2017",
                "openDt": "2017-10-20",
                "typeNm":"장편",
                "prdtStatNm":"기타",
                "nationAlt":"한국",
                "genreAlt":"드라마,가족",
                "repNationNm": "한국",
                "repGenreNm":"드라마"
            }
        }
    ]
}

 

URI 방식의 검색은 쿼리의 조건이 복잡하고 길어지기 때문에 여러 필드를 각기 다른 검색어로 질의하는 것이 어렵다. 그런 경우 아래와 같이 Request Body 방식의 검색을 사용하면 편리하다.

POST movie/_search
{
    "query" : {
        "term": {
            "typeNm": "장편"
        }
    }
}

 

쿼리 구문은 아래와 같이 여러 개의 키를 조합하여 객체의 키 값으로 사용할 수도 있다.

{
    size: # 몇 개의 결과를 반환할지 결정한다(기본값은 10).
    
    from: # 어느 위치부터 반환할지를 결정한다.
          # 0부터 시작하면 상위 0~10건의 데이터를 반환한다(기본값은 0).

    _source: # 특정 필드만 결과로 반환하고 싶을 때 사용한다.

    sort: # 특정 필드를 기준으로 정렬한다.
          # asc, desc로 오름차순, 내림차순 정렬을 지정할 수도 있다.

    query: {
        # 검색될 조건을 정의한다.
    }

    filter: {
        # 검색 결과 중 특정한 값을 다시 보여준다.
        # 결과 내에서 재검색할 때 사용하는 기능 중 하나다.
        # 다만 필터를 사용하게 되면 자동으로 score 값이 정렬되지 않는다.
    }

}

 

집계 api

엘라스틱서치 5.0 이후부터 독자적인 집계 api를 지원하게되는데 기본적으로 메모리 기반으로 동작하기 때문에 대용량의 데이터 통계 작업이 가능해진 시점이다. movie 인덱스의 문서를 장르별로 집계해보자.

POST /movie/ search?size=0
{
    "aggs":{
        "genre": {
            "terms": {
                "field": "genreAlt"
            }
        }
    }
}


# result
{
    "took": 10,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 63069,
        "max_score": 0,
        "hits": []
    },
    "aggregations": {
        "genre": {
            "doc_count_error_upper_bound": 291,
            "sum_other_doc_count": 21317,
            "buckets": [
                {
                    "key": "드라마",
                    "doc_count": 19856
                },
                {
                    "key": "장르없음",
                    "doc_count": 16426
                },
                {
                    "key": "코미디",
                    "doc_count": 6590
                },
                (・・・ 생략 ・・・)
                {
                    "key": "스릴러",
                    "doc_count": 4438
                }
            ]
        }
    }
}

버킷이라는 구조 안에 그룹화된 데이터가 포함되어 있다. 엘라스틱서치의 집계가 강력한 이유 중 하나는 버킷 안에 다른 버킷의 결과를 추가할 수 있다는 점이다. 이러한 특성을 이용해 다양한 집계 유형을 결합하거나 중첩, 조합할 수 있다.

POST movie/_search?size=0
{
    "aggs": {
        "genre": {
            "terms": {
                "field": "genreAlt"
            },
            "aggs": {
                "nation": {
                    "terms": {
                        "field": "nationAlt"
                    }
                }
            }
        }
    }
}

 

- 데이터 집계 타입 -

집계 기능은 서로 조합할 수 있는 4가지의 api로 제공된다.

  • 버킷 집계: 집계 중 가장 많이 사용한다. 문서의 필드를 기준으로 버킷을 집계한다.
  • 메트릭 집계: 문서에서 추출된 값을 가지고 sum, max, min, avg를 계산한다.
  • 매트릭스 집계: 행렬의 값을 합하거나 곱한다.
  • 파이프라인 집계: 버킷에서 도출된 결과 문서를 다른 필드 값으로 재분류한다. 즉 다른 집계의 결과를 다시 한 번 집계한다.
반응형

'Reading > 엘라스틱서치 실무 가이드' 카테고리의 다른 글

데이터 모델링: 메타 필드  (1) 2023.12.08
데이터 모델링: 맵핑 api  (2) 2023.12.05
엘라스틱서치 용어  (1) 2023.12.01
키바나 설치  (0) 2023.12.01
Elasticsearch 환경 구축  (2) 2023.12.01
Comments