# Elastic 가이드 북

> 이 가이드북은 출판을 위해 집필중이던 내용을 Elastic을 처음 시작하시는 분들께 도움이 되고 커뮤니티와 함께 완성 해 나가려는 목적으로 공개하게 되었습니다. 모든 문서에 대한 저작권은 저자인 [**김종민**](http://kimjmin.net)에게 있으며 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표들을 인용하고자 하는 경우 출처를 명시하고 **김종민(<kimjmin@gmail.com>)**&#xC5D0;게 사용 내용을 알려주시기 바랍니다.

## 목차

* [1. 서문](/01-overview)
  * [1.1 Elastic Stack 소개](/01-overview/1.1-elastic-stack)
    * [1.1.1 Elasticsearch](/01-overview/1.1-elastic-stack/1.1.1-elasticsearch)
    * [1.1.2 Logstash](/01-overview/1.1-elastic-stack/1.1.2-logstash)
    * [1.1.3 Kibana](/01-overview/1.1-elastic-stack/1.1.3-kibana)
    * [1.1.4 Beats](/01-overview/1.1-elastic-stack/1.1.4-beats)
* [2. Elasticsearch 시작하기](/02-install)
  * [2.1 데이터 색인](/02-install/2.1)
  * [2.2 설치 및 실행](/02-install/2.2)
    * [2.2.1 다운로드 설치 및 실행](/02-install/2.2/2.2.1-download-install)
    * [2.2.2 Unix RPM (yum) 설치 및 실행](/02-install/2.2/2.2.2-unix-rpm-yum)
    * [2.2.3 윈도우 운영체제에서 MSI 파일로 설치](/02-install/2.2/2.2.3-msi)
  * [2.3 elasticsearch 환경 설정](/02-install/2.3-elasticsearch)
    * [2.3.1 jvm.options](/02-install/2.3-elasticsearch/2.3.1-jvm.options)
    * [2.3.2 elasticsearch.yml](/02-install/2.3-elasticsearch/2.3.2-elasticsearch.yml)
    * [2.3.3 노드의 역할 : master, data, ingest, ml](/02-install/2.3-elasticsearch/2.3.3-node-settings)
    * [2.3.4 커맨드 라인 설정](/02-install/2.3-elasticsearch/2.3.4-cofig-on-start-command)
* [3. Elasticsearch 시스템 구조](/03-cluster)
  * [3.1 클러스터 구성](/03-cluster/3.1-cluster-settings)
  * [3.2 인덱스와 샤드 - Index & Shards](/03-cluster/3.2-index-and-shards)
  * [3.3 마스터 노드와 데이터 노드 - Master & Data Nodes](/03-cluster/3.3-master-and-data-nodes)
* [4. Elasticsearch 데이터 처리](/04-data)
  * [4.1 REST API](/04-data/4.1-rest-api)
  * [4.2 CRUD - 입력, 조회, 수정, 삭제](/04-data/4.2-crud)
  * [4.3 벌크 API - \_bulk API](/04-data/4.3-_bulk)
  * [4.4 검색 API - \_search API](/04-data/4.4-_search)
* [5. 검색과 쿼리 -  Query DSL](/05-search)
  * [5.1 풀 텍스트 쿼리 - Full Text Query](/05-search/5.1-query-dsl)
  * [5.2 Bool 복합 쿼리 - Bool Query](/05-search/5.2-bool)
  * [5.3 정확도 - Relevancy](/05-search/5.3-relevancy)
  * [5.4 Bool : Should](/05-search/5.4-keyword)
  * [5.5 정확값 쿼리 - Exact Value Query](/05-search/5.5-exact-value)
  * [5.6 범위 쿼리 - Range Query](/05-search/5.6-range)
* [6. 데이터 색인과 텍스트 분석](/06-text-analysis)
  * [6.1 역 인덱스 - Inverted Index](/06-text-analysis/6.1-indexing-data)
  * [6.2 텍스트 분석 - Text Analysis](/06-text-analysis/6.2-text-analysis)
  * [6.3 애널라이저 - Analyzer](/06-text-analysis/6.3-analyzer-1)
    * [6.3.1 \_analyze API](/06-text-analysis/6.3-analyzer-1/6.3-analyzer)
    * [6.3.2 Term 쿼리](/06-text-analysis/6.3-analyzer-1/6.3.1-term)
    * [6.3.3 사용자 정의 애널라이저 - Custom Analyzer](/06-text-analysis/6.3-analyzer-1/6.4-custom-analyzer)
    * [6.3.4 텀 벡터 - \_termvectors API](/06-text-analysis/6.3-analyzer-1/6.4.1-_termvectors-api)
  * [6.4 캐릭터 필터 - Character Filter](/06-text-analysis/6.4-character-filter)
    * [6.4.1 HTML Strip](/06-text-analysis/6.4-character-filter/6.4.1-html-strip)
    * [6.4.2 Mapping](/06-text-analysis/6.4-character-filter/6.4.2-mapping)
    * [6.4.3 Pattern Replace](/06-text-analysis/6.4-character-filter/6.4.3-pattern-replace)
  * [6.5 토크나이저 - Tokenizer](/06-text-analysis/6.5-tokenizer)
    * [6.5.1 Standard, Letter, Whitespace](/06-text-analysis/6.5-tokenizer/6.5.1-standard-letter-whitespace)
    * [6.5.2 UAX URL Email](/06-text-analysis/6.5-tokenizer/6.5.2-uax-url-email)
    * [6.5.3 Pattern](/06-text-analysis/6.5-tokenizer/6.5.3-pattern)
    * [6.5.4 Path Hierarchy](/06-text-analysis/6.5-tokenizer/6.5.4-path-hierarchy)
  * [6.6 토큰 필터 - Token Filter](/06-text-analysis/6.6-token-filter)
    * [6.6.1 Lowercase, Uppercase](/06-text-analysis/6.6-token-filter/6.6.1-lowercase-uppercase)
    * [6.6.2 Stop](/06-text-analysis/6.6-token-filter/6.6.2-stop)
    * [6.6.3 Synonym](/06-text-analysis/6.6-token-filter/6.6.3-synonym)
    * [6.6.4 NGram, Edge NGram, Shingle](/06-text-analysis/6.6-token-filter/6.6.4-ngram-edge-ngram-shingle)
    * [6.6.5 Unique](/06-text-analysis/6.6-token-filter/6.6.5-unique)
  * [6.7 형태소 분석 - Stemming](/06-text-analysis/6.7-stemming)
    * [6.7.1 Snowball](/06-text-analysis/6.7-stemming/6.7.1-snowball)
    * [6.7.2 노리 (nori) 한글 형태소 분석기](/06-text-analysis/6.7-stemming/6.7.2-nori)
* [7. 인덱스 설정과 매핑 - Settings & Mappings](/07-settings-and-mappings)
  * [7.1 설정 - Settings](/07-settings-and-mappings/7.1-settings)
  * [7.2 매핑 - Mappings](/07-settings-and-mappings/7.2-mappings)
    * [7.2.1 문자열 - text, keyword](/07-settings-and-mappings/7.2-mappings/7.2.1)
    * [7.2.2 숫자 - long, double ...](/07-settings-and-mappings/7.2-mappings/7.2.2)
    * [7.2.3 날짜 - date](/07-settings-and-mappings/7.2-mappings/7.2.3-date)
    * [7.2.4 불리언 - boolean](/07-settings-and-mappings/7.2-mappings/7.2.4-boolean)
    * [7.2.5 Object 와 Nested](/07-settings-and-mappings/7.2-mappings/7.2.5-object-nested)
    * [7.2.6 위치 정보 - Geo](/07-settings-and-mappings/7.2-mappings/7.2.6-geo)
    * [7.2.7 기타 필드 타입 - IP, Range, Binary](/07-settings-and-mappings/7.2-mappings/7.2.7-ip-range-binary)
  * [7.3 멀티 (다중) 필드 - Multi Field](/07-settings-and-mappings/7.3-multi-field)
* [8. 집계 - Aggregations](/08-aggregations)
  * [8.1 메트릭 - Metrics Aggregations](/08-aggregations/8.1-metrics-aggregations)
  * [8.2 버킷 - Bucket Aggregations](/08-aggregations/8.2-bucket-aggregations)
  * [8.3 하위 - sub-aggregations](/08-aggregations/8.3-aggregations)
  * [8.4 파이프라인 - Pipeline Aggregations](/08-aggregations/8.4-pipeline-aggregations)

{% hint style="info" %}
&#x20; 이 가이드북에 있는 대부분의 내용은 [Elastic 공식 도큐먼트](https://www.elastic.co/guide/index.html)를 참고하여 작성되었습니다.
{% endhint %}


# 1. 서문

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20;지금 세상은 데이터가 지배하고 있다고 해도 과언이 아닙니다. 매일 수많은 데이터가 범람하고 있고 이러한 데이터들의 분석과 처리는 세상 많은 문제해결의 중심이 되었습니다. 이런 데이터 홍수의 세상 속에서 Elasticsearch가 세상에 모습을 드러낸 지도 벌써 7년이 넘었습니다. Elasticsearch는 현재는 세상에서 가장 인기가 있는 오픈소스 검색엔진으로 수많은 개인 개발자, 기업 그리고 공공기관들로부터 사랑을 받고 있습니다.

&#x20;전문검색엔진 (Full-text search engine)으로 처음 개발되었지만, Elasticsearch는 검색엔진을 넘어 보안, 로그분석, 전문분석 등 다양한 영역에서 중요한 역할을 하고 있으며, 현재는 Kibana, Logstash, Beats들과 함께 다양한 전문 분야에서 수많은 문제들을 해결하고 있습니다. 필자가 처음 Elasticsearch를 접한 2013년만 해도 아직 1.0 버전이 나오기 전이었는데 2019년 현재는 7.x 버전까지 발표하면서 기업용 솔루션으로도 완전히 자리를 잡은 것을 볼 수 있습니다.

&#x20;이 책은 Elastic Stack 7.x 버전을 기준으로 하고 있으며 처음 Elasticsearch를 접하는 독자들 부터 Elastic Stack을 이용해 고급 기술을 사용하고자 하는 독자들에게 까지 다양하게 도움이 될 수 있도록 노력하였습니다. 이 책의 내용을 이해하려면 유닉스 시스템과 자바에 대한 기초 지식이 필요합니다. Elastic 기술들을 전반적으로 이해하는 데에 최대한 도움이 되는 순서대로 읽어나갈 수 있도록 하였습니다.


# 1.1 Elastic Stack 소개

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 포함된 자료를 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 알려주시기 바랍니다.

&#x20; 2004년 샤이 배논(Shay Banon)은 Compass 라는 이름의 오픈소스 검색엔진을 개발하였습니다. 샤이 배논이 처음 Compass 검색엔진 개발을 하게 된 계기는 요리 공부를 시작한 아내를 위해 레시피 검색 프로그램을 만들기 위해서였습니다. 레시피 검색 프로그램에 아파치 루씬(Apache Lucene)을 적용하려던 중 루씬이 가진 한계를 보완하기 위해 새로 검색엔진을 만들기 위한 프로젝트를 시작한 것이 계기입니다. 2010년 샤이는 Compass를 Elasticsearch라고 이름을 바꾸고 프로젝트를 오픈소스로 공개하였는데, 곧 수많은 검색 개발자들에게 인기를 얻으며 급속도로 성장하기 시작했습니다.\
*(그리고 샤이 배논의 아내는 아직도 레시피 프로그램을 기다리고 있습니다.)*

&#x20; Elasticsearch 프로젝트는 2012년에 창시자 샤이 배논과 함께 스티븐 셔르만(Steven Schuurman), 우리 보네스(Uri Boness) 그리고 아파치 루씬 커미터인 사이먼 윌너(Simon Willnauer) 4인의 멤버에 인해 네델란드 암스텔담에서 처음 회사로 설립되었으며 집필중인 2019년 현재는 주 본사인 네덜란드 암스테르담과 캘리포니아 마운틴 뷰를 비롯한 전 세계에 직원들이 분포되어 있습니다.

&#x20; Logstash, Kibana와 함께 사용 되면서 한동안 ELK Stack (Elasticsearch, Logstash, Kibana) 이라고 널리 알려지게 된 Elastic은 2013년에 Logstash, Kibana 프로젝트를 정식으로 흡수하여 한 지붕 아래에서 함께 개발을 해 나가고 있습니다. 2015년에는 회사명을 Elasticsearch 에서 Elastic으로 변경 하고, ELK Stack 대신 제품명을 Elastic Stack이라고 정식으로 명명하면서 모니터링, 클라우드 서비스, 머신러닝 등의 기능을 계속해서 개발, 확장 해 나가고 있습니다.


# 1.1.1 Elasticsearch

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 포함된 자료를 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 알려주시기 바랍니다.

![Elastic Stack의 심장 - https://www.elastic.co/kr/products/elasticsearch](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUs7k4juqOieCcJjHy%2F-Ln0SiCEjxEn-MpKIEsJ%2Fimage.png?alt=media\&token=6b3ebd18-04bc-4b1b-9401-e193d7946ee5)

&#x20; Elastic 홈페이지에서는 Elasticsearch를 Elastic Stack의 심장이라고 소개하고 있는 만큼 Elasticsearch는 전체 스택의 중심이며 가장 중요한 역할을 하고 있습니다. 기본적으로 모든 데이터를 색인하여 저장하고 검색, 집계 등을 수행하며 결과를 클라이언트 또는 다른 프로그램으로 전달하여 동작하게 합니다.

&#x20; Elasticsearch는 뛰어난 검색 능력과 대규모 분산 시스템을 구축할 수 있는 다양한 기능들을 제공하지만, 설치 과정과 사용 방법은 비교적 쉽고 간편합니다. 기업에서 뿐만 아니라 학교, 개인을 위한 프로젝트에도 다양하게 사용이 가능합니다. 기존 관계 데이터베이스 시스템에서는 다루기 어려운 전문검색(Full Text Search) 기능과 점수 기반의 다양한 정확도 알고리즘, 실시간 분석 등의 구현이 가능합니다. 또한 다양한 플러그인들을 사용해 손쉽게 기능의 혹장이 가능하며 아마존 웹 서비스(AWS), 마이크로소프트 애저(MS Azure) 같은 클라우드 서비스 그리고 하둡(Hadoop) 플랫폼들과의 연동도 가능합니다.

&#x20; 기본적으로 Elasticsearch 는 다음과 같은 특징들을 가지고 있습니다.

### 오픈소스 (open source)

&#x20; Elasticsearch의 핵심 기능들은 Apache 2.0 라이센스로 배포되고 있고 Elastic Stack의 모든 제품들은 (<https://github.com/elastic>) 깃헙 리파지토리 에서 소스들을 찾을 수 있습니다. 6.3 버전 부터는 Elastic 라이센스와 Apache 라이센스가 섞여 있지만 각각의 버전에 대해 별도 배포판이 존재하고 license 파일에서 어떤 경로의 파일들이 어떤 라이센스로 되어 있는지 확인이 가능합니다. 현재는 x-pack 디렉토리 아래 있는 파일들만이 Elastic 라이센스를 따르고 그 외의 파일들은 Apache 라이센스를 따릅니다.

&#x20; 루씬이 자바로 만들어졌기 때문에 Elasticsearch도 마찬가지로 자바로 코딩이 되어 있습니다. 루씬은 하둡을 개발한 더그 커팅(Doug Cutting)에 의해 처음 만들어졌지만 Elasticsearch 엔지니어들 중에는 루씬 커미터들이 다수 있어서 루씬을 매우 깊은 레벨에서 다루고 있고, Elastic 사의 루씬 커미터인 개발자들이 실제로 루씬 프로젝트의 절반 이상의 기능을 개발에 기여하고 있습니다. 이 사실은 Elastic사가 가장 자랑스럽게 여기는 사실입니다.

### 실시간 분석 (real-time)

&#x20; Elasticsearch의 가장 큰 특징 중 하나는 실시간(real-time) 분석 시스템 입니다. 현재 대용량 데이터 분석에 가장 널리 사용되고 있는 것은 하둡(Hadoop) 플랫폼 위에서 실행되는 Pig, Hive와 같은 다양한 맵 리듀서(Map reducer) 들입니다. 하둡은 기본적으로 배치 기반의 분석 시스템으로 분석에 사용될 소스 데이터, 분석을 수행 할 프로그램을 올려 놓고 분석을 실행하여 결과 셋이 나오도록 하는 하나의 루틴으로 실행됩니다.

&#x20; Elasticsearch는 하둡 시스템과 달리 Elasticsearch 클러스터가 실행되고 있는 동안에는 계속해서 데이터가 입력 (검색엔진에서는 색인 – indexing 이라고 표현합니다) 되고, 그와 동시에 실시간에 가까운 (near real-time) 속도로 색인된 데이터의 검색, 집계가 가능합니다.

### 전문(full text) 검색 엔진

&#x20; 루씬은 기본적으로 역파일 색인(inverted file index)라는 구조로 데이터를 저장합니다. 루씬을 사용하고 있는 Elasticsearch도 마찬가지로 색인된 모든 데이터를 역파일 색인 구조로 저장하여 가공된 텍스트를 검색합니다.. 이런 특성을 전문(full text) 검색이라고 합니다.

&#x20; JSON 문서 기반 Elasticsearch는 내부적으로는 역파일 색인 구조로 데이터를 저장하고 있으나, 사용자의 관점에서는 JSON 형식으로 데이터를 전달합니다. JSON형식은 간결하고 개발자들이 다루기 편한 구조로 되어 있어 색인 할 대상 문서를 가공 하거나 다른 클라이언트 프로그램과 연동하기에 용이합니다.

&#x20; 또한 key-value 형식이 아닌 문서 기반으로 되어 있기에 복합적인 정보를 포함하는 형식의 문서를 있는 그대로 저장이 가능하며 사용자가 직관적으로 이해하고 사용할 수 있습니다. Elasticsearch에서 질의에 사용되는 쿼리문이나 쿼리에 대한 결과도 모두 JSON 형식으로 전달되고 리턴됩니다.

&#x20; 다만 JSON이 Elasticsearch가 지원하는 유일한 형식이기 사전에 입력할 데이터를 JSON 형식으로 가공하는 것이 필요합니다. CSV, Apache log, syslog등과 같이 널리 사용되는 형식들은 Logstash에서 변환을 지원하고 있습니다.

### RESTFul API

&#x20; 현재 대규모 시스템들은 대부분 마이크로 서비스 아키텍처(MSA)를 기본으로 설계됩니다. 이러한 구조에 빠질 수 없는 것이 REST API와 같은 표준 인터페이스 입니다. Elasticsearch는 Rest API를 기본으로 지원하며 모든 데이터 조회, 입력, 삭제를 http 프로토콜을 통해 Rest API로 처리합니다.

### 멀티테넌시 (multitenancy)&#x20;

&#x20; Elasticsearch의 데이터들은 인덱스(Index) 라는 논리적인 집합 단위로 구성되며 서로 다른 저장소에 분산되어 저장됩니다. 서로 다른 인덱스들을 별도의 커넥션 없이 하나의 질의로 묶어서 검색하고, 검색 결과들을 하나의 출력으로 도출할 수 있는데, Elasticsearch의 이러한 특징을 멀티테넌시 라고 합니다.

&#x20; 창시자인 샤이 배논은 트위터와 공식 아이디로 kimchy 를 사용하고 있는데, 2000년대 초반 한국에서 생활한 적이 있어 김치를 좋아한다고도 이야기 하고 있고, 또 Kimchy는 샤이 배논의 어머니 성씨이기도 합니다. 샤이 배논은 동양 문화에 관심이 많으며 Elasticsearch 첫 로고는 소나무 분재였고 샤이 배논의 블로그에는 지금도 용(竜) 한자가 메인 로고로 있습니다.

![Elasticsearch 의 옛 로고들](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUs7k4juqOieCcJjHy%2F-Ln0W4xG_NqLNE0o9AI8%2Fimage.png?alt=media\&token=17f90fbc-53f4-4e8b-9827-64b6999448ae)


# 1.1.2 Logstash

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 포함된 자료를 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 알려주시기 바랍니다.

![데이터 집계, 변환, 저장 파이프라인 - https://www.elastic.co/kr/products/logstash](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUs7k4juqOieCcJjHy%2F-Ln0WfvbllD2LpEJg3zb%2Fimage.png?alt=media\&token=735ba825-9086-4f69-a22e-546526e6dcd9)

&#x20; 조던 시셀(Jordan Sissel)에 의해 탄생된 Logstash는 원래 Elasticsearch와 별개로 다양한 데이터 수집과 저장을 위해 개발된 프로젝트였습니다. 데이터의 색인, 검색 기능만을 제공하던 Elasticsearch는 데이터 수집을 위한 도구가 필요했는데, 때마침 Logstash가 출력 API로 Elasticsearch를 지원하기 시작하면서 많은 곳에서 Elasticsearch의 입력 수단으로 Logstash를 사용하기 시작했습니다. Elasticsearch와 Logstash는 서로 통합의 필요성을 느끼고 Logstash가 Elastic에 정식으로 합류하게 되어 하나의 스택으로 출범하게 되었습니다.&#x20;

&#x20; Logstash는 JRuby로 되어 있습니다. 루비 코드로 개발되어 자바의 런타임 머신 위에서 돌아갑니다. Elasticsearch와 마찬가지로 Apache 2.0 라이센스를 따르고 있어 자유롭게 사용이 가능합니다. Logstash는 데이터 처리를 위해서 크게 다음과 같은 과정들을 거치게 됩니다.

{% hint style="info" %}
입력(Inputs)  ➡️  필터(Filters)  ➡️  출력(Outputs)
{% endhint %}

&#x20; **입력** 기능에서 다양한 데이터 저장소로부터 데이터를 입력 받고 **필터** 기능을 통해 데이터를 확장, 변경, 필터링 및 삭제 등의 처리를 통해 가공을 합니다. 그 후 **출력** 기능을 통해 다양한 데이터 저장소로 데이터를 전송하게 됩니다.&#x20;

&#x20; Elasticsearch외에도 다양한 경로의 출력이 가능하기 때문에 Elasticsearch에 데이터를 색인하는 동시에 로컬 파일이나 아마존 AWS S3 저장소로 동시에 송출도 가능합니다. 그리고 Elasticsearch와 상관 없이 Redis의 데이터를 Kafka로 전송하는 경우 등과 같이 독자적으로 사용되기도 합니다.

&#x20; 지금은 로고 모양이 Elastic Stack 전체와 어울리는 모양으로 바뀌었지만 Logstash의 예전 로고는 통나무(log)가 콧수염(mustache)을 달고 있는 친근한 모습이었습니다. Log라는 영단어는 통나무 그리고 시계열 기록 이라는 이중적인 의미를 가지고 있고, 로고에는 콧수염으로도 표현되었지만 실제 stash라는 단어는 물건을 어딘가에 쌈(보관)하다는 의미를 가지고 있어 Logstash라는 이름은 이 도구가 하는 역할을 더할 나위 없이 잘 표현하는 이름입니다.

![Logstash 옛 로고](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUs7k4juqOieCcJjHy%2F-Ln0XEJAirFjoEvVd4f6%2Fimage.png?alt=media\&token=6ece34b5-b6fb-464b-872f-ed7440437011)


# 1.1.3 Kibana

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 포함된 자료를 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 알려주시기 바랍니다.

![](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUs7k4juqOieCcJjHy%2F-Ln0Zof4T0hzE9KJc73R%2Fimage.png?alt=media\&token=bb05c335-4c01-49d8-b13a-d3d74b4ef8e5)

&#x20; 어떤 도구가 사용자들에게 선택되기 위한 가장 중요한 요소 중 하나가 바로 시각화 입니다. 다행이 Elasticsearch는 Restful 한 속성과 JSON 문서 기반의 통신을 지원하기에 http 프로토콜을 이용해 어떤 클라이언트와도 손쉽게 연동이 가능합니다. 그래서 개발자들이 Elasticsearch와 연동되는 다양한 시각화 도구를 개발하였고, 이 중 라시드 칸(Rashid Khan)이 개발한 Kibana 라는 시각화 도구가 큰 인기를 끌었습니다.

&#x20; Kibana는 Elasticsearch를 가장 쉽게 시각화 할 수 있는 도구입니다. 검색, 그리고 aggregation의 집계 기능을 이용해 Elasticsearch로 부터 문서, 집계 결과 등을 불러와 웹 도구로 시각화를 합니다. **Discover**, **Visualize**, **Dashboard** 3개의 기본 메뉴와 다양한 App 들로 구성되어 있고, 플러그인을 통해 App의 설치가 가능합니다.

### Discover

&#x20; Discover는 Elasticsearch에 색인된 소스 데이터들의 검색을 위한 메뉴입니다. 검색 창에 질의문을 통해 데이터를 간편하게 검색, 필터링 할 수 있으며, 검색된 데이터의 원본 문서를 확인하거나 보고 싶은 필드만 선택해서 테이블 형태로 조회가 가능합니다. 시계열(time series) 기반의 로그 데이터인 경우 시간 히스토그램 그래프를 통해 시간대별 로그 수도 표시됩니다.

![](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUs7k4juqOieCcJjHy%2F-Ln0a0NvU10Mb6Rbk-Hs%2Fimage.png?alt=media\&token=9187d728-493b-470e-9156-4ed3b9b9f420)

### Visualize

&#x20; Visualize는 aggregation 집계 기능을 통해 조회된 데이터의 통계를 다양한 차트로 표현할 수 있는 패널을 만드는 메뉴입니다. 영역차트, 바차트, 파이차트, 라인차트 등 다양한 시각화 도구들의 사용이 가능하며 여기서 만들어진 패널들을 조합해서 대시보드를 만들게 됩니다.

![](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUs7k4juqOieCcJjHy%2F-Ln0aPn0YSjTxnokTxKc%2Fimage.png?alt=media\&token=152479dd-2141-48d5-a70c-aea3db3ac764)

![](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUs7k4juqOieCcJjHy%2F-Ln0alkzB9B9OgG9Thby%2Fimage.png?alt=media\&token=6fdb04bd-f371-4af8-ba22-6f91c3691845)

### Dashboard

&#x20; Visualize 메뉴에서 만들어진 시각화 도구들을 조합해서 대시보드 화면을 만들고 저장, 불러오기 등을 할 수 있는 메뉴입니다. 다른 메뉴들과 마찬가지로 검색 창에 쿼리를 입력하거나 시각화 도구들을 클릭해서 조회할 데이터들의 필터링이 가능하고, URL로 대시보드를 다른 사람들과 공유하거나 json 형식으로 내보내고 불러오기 등이 가능합니다.

![Kibana 대시보드 화면](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUs7k4juqOieCcJjHy%2F-Ln0bNhB9DKRNGdbC8a9%2Fimage.png?alt=media\&token=a7dda686-ee1f-4455-97f8-ecbe222bf68a)

&#x20; 그밖에도 Kibana 5.x 버전부터는 이전 버전에서는 플러그인으로 추가해야 했던 Timelion이 기본으로 내장되었으며, 간편한 개발 도구였던 Sense역시 Dev Tools 라는 이름의 메뉴로 업그레이드 되어 기본으로 내장되었습니다. 또한 Elastic에서 제공하는 유료 플러그인인 X-Pack을 설치하게 되면 Monitoring 기능을 이용해서 Elasticsearch 클러스터 상태를 모니터링 하거나, Graph 모듈을 이용해서 관계도 분석이 가능하고, Reporting을 사용한 Kibana화면의 PDF 출력도 가능합니다.

&#x20; 6.3 버전 부터는 X-Pack 코드들이 배포판에 기본으로 내장되어 별도 플러그인 설치 절차 필요 없이 라이센스 파일을 적용하는 것 만으로 사용할 수 있게 되었습니다. 현재는 X-Pack 이라는 이름을 사용하지 않고 단순히 Elastic 의 기능들 이라고만 명명하고 있습니다.

&#x20; 여담으로 Kibana 역시 현재는 Elastic Stack에 어우러지는 로고로 디자인 되었지만 초기에는 아프리카의 움집 모양의 로고를 사용했습니다. Kibana창시자인 라시드는 Elasticsearch시각화 도구를 만들면서 처음에는 수집기인 Logstash의 통나무 로고와 잘 어울릴만한 이름인 영문 오두막 캐빈(cabin)을 생각했습니다. 하지만 흔한 명사라 좀 더 특이한 이름을 고민하며 cabin을 다른 나라 언어로 하면 어떨까 생각했고, 구글 번역기로 여러 언어로 번역 해 보던 중 아프리카 스와힐리어로 오두막이 kibana라는 것을 찾고 나서 마음에 들어 이 이름을 선택했습니다. 하지만 이것은 구글 번역기의 오류였고 실제 스와힐리어로 오두막은 키바나(kibana)가 아닌 키반다(kibanda) 였습니다.

![Kibana 의 초기 버전 로고](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUs7k4juqOieCcJjHy%2F-Ln0bzYrDtY4M8J5l7U6%2Fimage.png?alt=media\&token=ea3d1e8c-1e83-48e5-9cac-d21c3ec3f392)

&#x20; 추가적으로 Kibana는 일본에 있는 기차역 이름이기도 하고, 바르셀로나에 있는 레스토랑의 이름이기도 합니다.


# 1.1.4 Beats

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 포함된 자료를 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 알려주시기 바랍니다.

![경량 데이터 수집기 - https://www.elastic.co/kr/products/beats](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUuYXKolVtg6b3LMXw%2F-Ln2h9ipkbx08RG2XMAM%2Fimage.png?alt=media\&token=09081d24-92d9-4106-be85-db44128a9bce)

&#x20; Logstash가 데이터 수집기로서의 역할을 훌륭하게 해 내고 있지만 너무 다양한 기능 때문에 프로그램의 부피가 컸고 실행하는 데에 꽤 많은 자원을 필요로 했습니다. Elasticsearch 클러스터로의 대용량 데이터 전송은 보통 하나의 소스가 아닌 다양한 시스템들로부터 수집을 하였기에 그 모든 단말 시스템에 Logstash를 설치하는 것은 적지 않은 부담이었습니다. 그래서 Logstash팀은 단말 시스템으로 부터 데이터를 수집하고 필터기능 없이 가볍게 Elasticsearch 또는 Logstash로 데이터를 전송하는 Lumberjack 이라는 이름의 일종의 포워더 성격의 원격 수집기를 개발 중에 있었습니다.

&#x20; 그러던 중 독일에서 어느 두 개발자들이 Packetbeat라는 프로그램을 이용해 네트워크 패킷을 스니핑하여 Elasticsearch에 저장하여 모니터링 하는 시스템을 공개했는데, 샤이 배논은 독일로 날아가 그 개발자들을 만나 Beats 프로젝트를 소개받게 됩니다. 튜더 골루벤코(Tudor Golubenco)와 모니카 사부(Monica Sarbu) 두 부부 개발자는 원래 패킷 스니핑 프로그램을 전문으로 개발하고 있었습니다. 수집한 패킷들을 어디에 저장할까 고민하던 중에 Elasticsearch에 이것들을 색인해서 Kibana로 모니터링 하면 꽤 멋진 시스템이 된다고 생각하여 그렇게 실행하였고 이것을 오픈소스로 공개하였습니다.

&#x20; 샤이 배논은 Elastic Stack에 부족한 꼭지를 Beats가 채워줄 수 있을거라 믿고 이 두 개발자 부부와 Beats 프로젝트를 Elastic에 합류시킵니다. 개발 중이던 Logstash 원격 수집기 프로젝트는 중단되었고 그 역할을 Beats가 대신 실행하게 되었습니다. Beats는 구글에서 개발된 Go 언어로 개발되었습니다. Go는 매우 가볍고 바이너리 실행 파일로 컴파일 되는 언어라 라이브러리 종속성도 적습니다. 현재 Elastic 에서는 Packetbeat, Libbeat, Filebeat, Metricbeat, Winlogbeat, Auditbeat 등을 개발하여 배포하고 있으며 전 세계 오픈소스 개발자들로부터 50여가지 이상의 Beats 들이 개발되고 있습니다.

### Libbeat

&#x20; 먼저 Beats 팀은 처음 개발한 PacketBeat 프로그램에서 Elasticsearch로 전송하는 부분만을 따로 추출하여 일종의 공통 라이브러리로 만들었습니다. 특정한 데이터를 수집하는 부분만 코딩 하고 나면 데이터를 JSON 문서로 변환하고, 데이터가 유실되지 않게 관리하고, Elasticsearch로 전송하는 역할은 이 공통 라이브러리인 Libbeat이 담당하게 됩니다.

### Packetbeat

&#x20; 가장 처음 존재했던 Beat이 바로 Packetbeat 입니다. 설치된 시스템에 유통되는 패킷들을 스니핑 해서 Elasticsearch에 적재시키는 기능을 합니다.

### Filebeat

&#x20; 사용자들이 가장 필요로 하는 기능은 파일의 내용을 수집하는 기능입니다. Web log 또는 machine log 등이 저장되는 파일 경로를 지정하기만 하면 Filebeat은 해당 경로에 적재되는 파일을 읽어들이며 새로운 내용이 추가될 때 마다 그 내용을 Elasticsearch로 색인합니다.

### Metricbeat

&#x20; Metricbeat은 실행시켜놓기만 하면 시스템에서 실행중인 프로세스들의 정보와 이 프로세스들이 소모중인 CPU, Memory 등에 대한 상태들을 수집해서 Elasticsearch에 적재하고 손쉽게 이것들을 모니터링 할 수 있는 시스템을 만들 수 있습니다.

### Winlogbeat

&#x20; Winlogbeat은 Microsoft Windows 기반 시스템에서 시스템에 적재되는 Windows event 들을 수집해 Elasticsearch로 색인하여 모니터링 할 수 있도록 합니다.

### Auditbeat

&#x20; Auditbeat은 리눅스 시스템의 사용자 접속과 실행 이벤트 로그들과 같은 감사 데이터를 수집합니다. 주로 시스템의 보안 분석을 할 때 사용됩니다.

### Heartbeat

&#x20; 다른 Beats 들도 이름에서 손쉽게 어떤 역할을 하는지 짐작이 가능한데, Heartbeat은 Beats 의 종류이면서도 심장 박동과 동일한 단어이기 때문에 재미있습니다. 역시 이름에서 유추할 수 있듯이 Heartbeat은 다른 프로세스들의 가동 시간 등을 모니터링 합니다. ICMP, TCP, HTTP 프로토콜 등을 통해 Ping 명령으로 원격의 프로세스의 가동 여부를 확인하는데, 동작은 단순하지만 다양한 시스템을 동시에 모니터링 할 때 매우 유용합니다.

### Functionbeat

&#x20; 가장 최근에 추가된 Functionbeat은 요즘 유행하는 마이크로 서비스 아키텍쳐(MSA)와 같은 FaaS 클라우드 기반의 시스템에서 서버리스 프레임워크를 이용하여 클라우드 인프라를 모니터링 합니다. 다른 Beats 들과 달리 수집을 위한 데이터가 있는 시스템에 설치되는 것이 아니라 Lamda와 같은 기능으로 배포됩니다.

&#x20; Beats 역시 현재는 Elastic Stack에 맞는 로고로 다시 디자인 되었지만 처음 로고는 모니카가 디자인 했던 물고기 였습니다.

![Beats의 옛 로고](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUuYXKolVtg6b3LMXw%2F-Ln2lQZ13xwjKbwJ9gu1%2Fimage.png?alt=media\&token=6026b7e3-f284-414e-b97c-510c127b0c7a)


# 2. Elasticsearch 시작하기

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 이번 장 부터는 본격적으로 Elasticsearch를 다루어 보도록 하겠습니다. 먼저 데이터 색인과 REST API 구조에 대해서 조금 더 설명한 후에 Elasticsearch를 설치하고 실행 해 보도록 하겠습니다.

&#x20; Elasticsearch는 자바로 개발되었기 때문에 자바 실행이 가능한 환경이라면 어디서든 구동이 가능합니다. 보통은 유닉스 운영체제에서의 구동을 기준으로 하기 때문에 이 책에서도 유닉스 환경을 기준으로 설명을 진행하겠습니다. 리눅스 또는 맥OS 같은 유닉스 계열 운영체제의 사용자는 앞으로 책에서 설명할 내용을 따라 실습을 하면 됩니다. 윈도우 운영체제 사용자는 뒤에서 설명할 Kibana의 Dev Tool을 가지고 실습을 하면 됩니다.


# 2.1 데이터 색인

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 검색기술을 다루다 보면 검색과 색인이라는 단어를 자주 만나게 됩니다. 특히 아파치 루씬, 그리고 Elasticsearch와 관련해서는 같은 단어가 여러 뜻으로 혼용되어 쉽게 헷갈릴 수 있으므로 혼란을 방지하기 위해 몇가지 중요한 개념의 용어들을 우선 정리하고 가도록 하겠습니다.

* **\[동사] 색인 (indexing)** : 데이터가 검색될 수 있는 구조로 변경하기 위해 원본 문서를 검색어 토큰들으로 변환하여 저장하는 일련의 과정입니다. 이 책에서는 색인 또는 색인 과정이라고 표기합니다.
* **\[명사] 인덱스 (index, indices)** : 색인 과정을 거친 결과물, 또는 색인된 데이터가 저장되는 저장소입니다. 또한 Elasticsearch에서 도큐먼트들의 논리적인 집합을 표현하는 단위이기도 합니다. 이 책에서는 인덱스라고 표기합니다.
* **검색 (search)** : 인덱스에 들어있는 검색어 토큰들을 포함하고 있는 문서를 찾아가는 과정입니다.
* **질의 (query)** : 사용자가 원하는 문서를 찾거나 집계 결과를 출력하기 위해 검색 시 입력하는 검색어 또는 검색 조건입니다. 이 책에서는 질의 또는 쿼리라고 표현합니다.

![](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUuxSw7YcAaimkQNct%2F-Ln9wH3l50nFl5Q8l70v%2Fimage.png?alt=media\&token=0c46ba3d-6bdf-481b-82d3-cea12a7eda9d)


# 2.2 설치 및 실행

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Elasticsearch는 홈페이지 ([https://www.elastic.co](https://www.elastic.co\)의))의 Product 또는 다운로드 메뉴를 통해 다운로드를 할 수 있습니다. 다음과 같이 ZIP, TAR, DEB, RPM 등의 다운로드 파일과 yum, apt-get 등의 설치에 대한 메뉴얼이 있습니다.

![Elasticsearch 다운로드 페이지 - https://www.elastic.co/downloads/elasticsearch](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUvI3ko4fvqdhDsao3%2F-Ln9wvnsuohUKRl3h79E%2Fimage.png?alt=media\&token=9e8863a4-dc2a-4d01-994b-4cbb4ef86f25)

&#x20; ZIP 또는 TAR 파일을 내려받아 압축을 풀고, 생성된 bin 디렉토리 아래에 있는 elasticsearch (Windowns의 경우 elasticsearch.bat) 파일을 실행합니다. 데비안이나 레드햇 리눅스 시스템의 경우 DEB, RPM 파일을 내려받아 백그라운드 서비스로 실행도 가능합니다.

&#x20; 6.3 버전 부터는 아파치 2.0 라이센스와 Elastic 라이센스 (X-Pack) 가 혼합된 배포판을 기본으로 제공하기 때문에 아파치 2.0 라이센스에 속한 기능만을 내려받으려면 OSS 페이지로 이동해서 내려받아야 합니다.

![Apache 2.0 라이센스 버전 - https://www.elastic.co/downloads/elasticsearch-oss](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUvI3ko4fvqdhDsao3%2F-Ln9xR5Hu-G9KPZ1Xih0%2Fimage.png?alt=media\&token=e12cbff5-4300-4d15-bc8b-7d2355cdac17)


# 2.2.1 다운로드 설치 및 실행

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Elasticsearch 실행을 위해서는 자바1.8 이상의 버전이 설치되어 있어야 하며 JAVA\_HOME 환경변수가 잡혀있어야 합니다. 각 버전별로 필요한 자바 버전은 <https://www.elastic.co/support/matrix#matrix_jvm> 페이지에서 확인이 가능합니다.

{% hint style="info" %}
Elasticsearch 7.0 버전 부터는 기본 배포판에 open-jdk 가 포함되어 있어 따로 Java를 설치 해 주지 않아도 됩니다. 대신 운영체제에 맞게 배포판을 받아야 합니다.
{% endhint %}

&#x20; 먼저 zip / tar.gz 배포판 설치 방법을 알아보겠습니다. 우선은 Linux / MacOS 기준으로 설명 드리겠습니다. 다운로드 한 파일의 압축을 푼 경로로 이동해서 bin/elasticsearch를 실행 해 보면 콘솔에서 다음과 같이 실행이 됩니다. 윈도우 운영체제의 경우 bin\elasticsearch.bat 를 실행하면 됩니다.

{% code title="Elasticsearch 실행 화면" %}

```bash
$ bin/elasticsearch
[2019-08-26T07:52:09,090][INFO ][o.e.e.NodeEnvironment    ] [Jongminui-MacBook-Pro.local] using [1] data paths, mounts [[/ (/dev/disk1s1)]], net usable_space [89.1gb], net total_space [465.6gb], types [apfs]
[2019-08-26T07:52:09,106][INFO ][o.e.e.NodeEnvironment    ] [Jongminui-MacBook-Pro.local] heap size [989.8mb], compressed ordinary object pointers [true]
[2019-08-26T07:52:09,169][INFO ][o.e.n.Node               ] [Jongminui-MacBook-Pro.local] node name [Jongminui-MacBook-Pro.local], node ID [RDBLYDInSxmMV1PEVit_pQ], cluster name [elasticsearch]
[2019-08-26T07:52:09,169][INFO ][o.e.n.Node               ] [Jongminui-MacBook-Pro.local] version[7.3.0], pid[38788], build[default/tar/de777fa/2019-07-24T18:30:11.767338Z], OS[Mac OS X/10.14.6/x86_64], JVM[Oracle Corporation/Java HotSpot(TM) 64-Bit Server VM/1.8.0_151/25.151-b12]
[2019-08-26T07:52:09,170][INFO ][o.e.n.Node               ] [Jongminui-MacBook-Pro.local] JVM home [/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre]
[2019-08-26T07:52:09,170][INFO ][o.e.n.Node               ] [Jongminui-MacBook-Pro.local] JVM arguments [-Xms1g, -Xmx1g, -XX:+UseConcMarkSweepGC, -XX:CMSInitiatingOccupancyFraction=75, -XX:+UseCMSInitiatingOccupancyOnly, -Des.networkaddress.cache.ttl=60, -Des.networkaddress.cache.negative.ttl=10, -XX:+AlwaysPreTouch, -Xss1m, -Djava.awt.headless=true, -Dfile.encoding=UTF-8, -Djna.nosys=true, -XX:-OmitStackTraceInFastThrow, -Dio.netty.noUnsafe=true, -Dio.netty.noKeySetOptimization=true, -Dio.netty.recycler.maxCapacityPerThread=0, -Dlog4j.shutdownHookEnabled=false, -Dlog4j2.disable.jmx=true, -Djava.io.tmpdir=/var/folders/0d/m7m670h13pz3lvr9xjz07zk80000gn/T/elasticsearch-785170093085857996, -XX:+HeapDumpOnOutOfMemoryError, -XX:HeapDumpPath=data, -XX:ErrorFile=logs/hs_err_pid%p.log, -XX:+PrintGCDetails, -XX:+PrintGCDateStamps, -XX:+PrintTenuringDistribution, -XX:+PrintGCApplicationStoppedTime, -Xloggc:logs/gc.log, -XX:+UseGCLogFileRotation, -XX:NumberOfGCLogFiles=32, -XX:GCLogFileSize=64m, -Dio.netty.allocator.type=unpooled, -XX:MaxDirectMemorySize=536870912, -Des.path.home=/Users/kimjmin/elastic/getStart/elasticsearch-7.3.0, -Des.path.conf=/Users/kimjmin/elastic/getStart/elasticsearch-7.3.0/config, -Des.distribution.flavor=default, -Des.distribution.type=tar, -Des.bundled_jdk=true]
[2019-08-26T07:52:11,000][INFO ][o.e.p.PluginsService     ] [Jongminui-MacBook-Pro.local] loaded module [aggs-matrix-stats]
[2019-08-26T07:52:11,000][INFO ][o.e.p.PluginsService     ] [Jongminui-MacBook-Pro.local] loaded module [analysis-common]
[2019-08-26T07:52:11,000][INFO ][o.e.p.PluginsService     ] [Jongminui-MacBook-Pro.local] loaded module [data-frame]
...
```

{% endcode %}

&#x20; 이제 Elasticsearch의 설치와 실행이 끝났습니다. (정말입니다!) 위 실행 화면을 보면 \[Jongminui-MacBook-Pro.local] 라는 이름으로 Elasticsearch의 노드가 실행 된 것을 볼 수 있습니다. 노드 이름은 직접 지정이 가능하며 지정하지 않았다면 7.0 버전 부터는 호스트명으로 생성되고 5.x, 6.x 버전에서는 노드 프로세스의 UUID의 첫 7자 알파벳으로 지정됩니다. Elasticsearch 2.x 이전 버전에서느 실행을 시키면 노드명이 \[Iron Man] 같은 아메리카코믹의 슈퍼영웅 이름으로 랜덤하게 생성 되었는데 저작권 문제 때문에 5.x 이상 부터는 디폴트 네임으로 사용이 불가능하게 되었습니다.

&#x20; Elasticsearch를 실행할 때 추가적으로 -d, -p 옵션을 사용할 수 있습니다.

* -**d** : Elasticsearch를 백그라운 데몬으로 실행합니다.
* **-p <파일명>** : Elasticsearch 프로세스 ID를 지정한 파일에 저장합니다. 실행이 종료되면 저장된 파일은 자동으로 삭제됩니다.

### -d : 백그라운드 실행

&#x20; **-d** 옵션을 추가해서 Elasticsearch를 실행해보면 화면에 아무런 반응 없이 명령 수행이 끝나게 되고, 실행 중인 Elasticsearch의 실행 로그는 logs 디렉토리 아래에 <클러스터명>.log 파일에서 확인이 가능합니다. 아무런 설정을 하지 않았다면 기본적으로 logs/elasticsearch.log 에 저장됩니다.

&#x20; 백그라운드로 실행한 후 ps -ef | grep elasticsearch 명령으로 실행 중인 프로세스를 검색하면 Elasticsearch가 실행되고 있음을 확인할 수 있습니다.

{% code title="ps 명령으로 백그라운드로 실행중인 Elasticsearch프로세스 검색" %}

```bash
$ bin/elasticsearch -d

$ ps -ef | grep elasticsearch
  501 38850     1   0  8:00AM ttys000    0:36.58 /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/bin/java -Xms1g -Xmx1g -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -Des.networkaddress.cache.ttl=60 -Des.networkaddress.cache.negative.ttl=10 -XX:+AlwaysPreTouch -Xss1m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djna.nosys=true -XX:-OmitStackTraceInFastThrow -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true -Dio.netty.recycler.maxCapacityPerThread=0 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Djava.io.tmpdir=/var/folders/0d/m7m670h13pz3lvr9xjz07zk80000gn/T/elasticsearch-7812365538420855556 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=data -XX:ErrorFile=logs/hs_err_pid%p.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -Xloggc:logs/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=32 -XX:GCLogFileSize=64m -Dio.netty.allocator.type=unpooled -XX:MaxDirectMemorySize=536870912 -Des.path.home=/Users/kimjmin/elastic/getStart/elasticsearch-7.3.0 -Des.path.conf=/Users/kimjmin/elastic/getStart/elasticsearch-7.3.0/config -Des.distribution.flavor=default -Des.distribution.type=tar -Des.bundled_jdk=true -cp /Users/kimjmin/elastic/getStart/elasticsearch-7.3.0/lib/* org.elasticsearch.bootstrap.Elasticsearch -d
  501 38857 38850   0  8:00AM ttys000    0:00.03 /Users/kimjmin/elastic/getStart/elasticsearch-7.3.0/modules/x-pack-ml/platform/darwin-x86_64/bin/controller
  501 38864 38857   0  8:00AM ttys000    0:00.52 ./autodetect --jobid=multiple --bucketspan=900 --lengthEncodedInput --maxAnomalyRecords=500 --timefield=@timestamp --persistInterval=12869 --maxQuantileInterval=23669 --limitconfig=/var/folders/0d/m7m670h13pz3lvr9xjz07zk80000gn/T/elasticsearch-7812365538420855556/limitconfig2644434340400277937.conf --quantilesState=/var/folders/0d/m7m670h13pz3lvr9xjz07zk80000gn/T/elasticsearch-7812365538420855556/multiple_quantiles_557343876454452728180.json --deleteStateFiles --fieldconfig=/var/folders/0d/m7m670h13pz3lvr9xjz07zk80000gn/T/elasticsearch-7812365538420855556/fieldconfig1365288688663343555.conf --logPipe=/var/folders/0d/m7m670h13pz3lvr9xjz07zk80000gn/T/elasticsearch-7812365538420855556/autodetect_multiple_log_38850 --input=/var/folders/0d/m7m670h13pz3lvr9xjz07zk80000gn/T/elasticsearch-7812365538420855556/autodetect_multiple_input_38850 --inputIsPipe --output=/var/folders/0d/m7m670h13pz3lvr9xjz07zk80000gn/T/elasticsearch-7812365538420855556/autodetect_multiple_output_38850 --outputIsPipe --restore=/var/folders/0d/m7m670h13pz3lvr9xjz07zk80000gn/T/elasticsearch-7812365538420855556/autodetect_multiple_restore_38850 --restoreIsPipe --persist=/var/folders/0d/m7m670h13pz3lvr9xjz07zk80000gn/T/elasticsearch-7812365538420855556/autodetect_multiple_persist_38850 --persistIsPipe
  501 38867 38460   0  8:00AM ttys000    0:00.00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn elasticsearch
  
$ head logs/elasticsearch.log
[2019-08-26T07:52:09,090][INFO ][o.e.e.NodeEnvironment    ] [Jongminui-MacBook-Pro.local] using [1] data paths, mounts [[/ (/dev/disk1s1)]], net usable_space [89.1gb], net total_space [465.6gb], types [apfs]
[2019-08-26T07:52:09,106][INFO ][o.e.e.NodeEnvironment    ] [Jongminui-MacBook-Pro.local] heap size [989.8mb], compressed ordinary object pointers [true]
[2019-08-26T07:52:09,169][INFO ][o.e.n.Node               ] [Jongminui-MacBook-Pro.local] node name [Jongminui-MacBook-Pro.local], node ID [RDBLYDInSxmMV1PEVit_pQ], cluster name [elasticsearch]
[2019-08-26T07:52:09,169][INFO ][o.e.n.Node               ] [Jongminui-MacBook-Pro.local] version[7.3.0], pid[38788], build[default/tar/de777fa/2019-07-24T18:30:11.767338Z], OS[Mac OS X/10.14.6/x86_64], JVM[Oracle Corporation/Java HotSpot(TM) 64-Bit Server VM/1.8.0_151/25.151-b12]
[2019-08-26T07:52:09,170][INFO ][o.e.n.Node               ] [Jongminui-MacBook-Pro.local] JVM home [/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre]
[2019-08-26T07:52:09,170][INFO ][o.e.n.Node               ] [Jongminui-MacBook-Pro.local] JVM arguments [-Xms1g, -Xmx1g, -XX:+UseConcMarkSweepGC, -XX:CMSInitiatingOccupancyFraction=75, -XX:+UseCMSInitiatingOccupancyOnly, -Des.networkaddress.cache.ttl=60, -Des.networkaddress.cache.negative.ttl=10, -XX:+AlwaysPreTouch, -Xss1m, -Djava.awt.headless=true, -Dfile.encoding=UTF-8, -Djna.nosys=true, -XX:-OmitStackTraceInFastThrow, -Dio.netty.noUnsafe=true, -Dio.netty.noKeySetOptimization=true, -Dio.netty.recycler.maxCapacityPerThread=0, -Dlog4j.shutdownHookEnabled=false, -Dlog4j2.disable.jmx=true, -Djava.io.tmpdir=/var/folders/0d/m7m670h13pz3lvr9xjz07zk80000gn/T/elasticsearch-785170093085857996, -XX:+HeapDumpOnOutOfMemoryError, -XX:HeapDumpPath=data, -XX:ErrorFile=logs/hs_err_pid%p.log, -XX:+PrintGCDetails, -XX:+PrintGCDateStamps, -XX:+PrintTenuringDistribution, -XX:+PrintGCApplicationStoppedTime, -Xloggc:logs/gc.log, -XX:+UseGCLogFileRotation, -XX:NumberOfGCLogFiles=32, -XX:GCLogFileSize=64m, -Dio.netty.allocator.type=unpooled, -XX:MaxDirectMemorySize=536870912, -Des.path.home=/Users/kimjmin/elastic/getStart/elasticsearch-7.3.0, -Des.path.conf=/Users/kimjmin/elastic/getStart/elasticsearch-7.3.0/config, -Des.distribution.flavor=default, -Des.distribution.type=tar, -Des.bundled_jdk=true]  
```

{% endcode %}

&#x20; 백그라운드로 실행 중인 Elasticsearch 프로세스를 종료하려면 kill 명령을 사용해야 합니다. 위에서 현재 실행되고 있는 엘라스틱서치의 프로세스 ID는 38850 입니다. **kill 38850** 명령으로 엘라스틱서치를 종료하고 다시 프로세스를 확인하면 프로세스가 종료된 것을 확인할 수 있습니다.

{% code title="kill 명령으로 엘라스틱서치 프로세스 종료" %}

```
$ kill 38850

$ ps -ef | grep elasticsearch
  501 38901 38460   0  8:03AM ttys000    0:00.00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn elasticsearch
```

{% endcode %}

### -p : 프로세스 ID 파일로 저장

&#x20; **-p <파일명>** 옵션을 추가해 실행된 Elasticsearch 프로세스 ID를 특정 파일에 저장할 수 있습니다. es.pid라는 파일에 실행된 Elasticsearch 프로세스 ID를 저장해 보겠습니다. 명령을 실행한 뒤 es.pid 파일의 내용을 확인하고 실행 중인 프로세스와 비교해 보겠습니다.

{% code title="-p 옵션으로 es.pid 파일에 프로세스 ID 저장" %}

```bash
$ bin/elasticsearch -d -p es.pid

$ ls
LICENSE.txt    README.textile config         es.pid         lib            modules
NOTICE.txt     bin            data           jdk            logs           plugins

$ cat es.pid
39060

$ ps -ef | grep elasticsearch
  501 39060     1   0  8:07AM ttys000    0:39.02 /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/bin/java -Xms1g -Xmx1g -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -Des.networkaddress.cache.ttl=60 -Des.networkaddress.cache.negative.ttl=10 -XX:+AlwaysPreTouch -Xss1m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djna.nosys=true -XX:-OmitStackTraceInFastThrow -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true -Dio.netty.recycler.maxCapacityPerThread=0 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Djava.io.tmpdir=/var/folders/0d/m7m670h13pz3lvr9xjz07zk80000gn/T/elasticsearch-7723664224795657363 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=data -XX:ErrorFile=logs/hs_err_pid%p.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -Xloggc:logs/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=32 -XX:GCLogFileSize=64m -Dio.netty.allocator.type=unpooled -XX:MaxDirectMemorySize=536870912 -Des.path.home=/Users/kimjmin/elastic/getStart/elasticsearch-7.3.0 -Des.path.conf=/Users/kimjmin/elastic/getStart/elasticsearch-7.3.0/config -Des.distribution.flavor=default -Des.distribution.type=tar -Des.bundled_jdk=true -cp /Users/kimjmin/elastic/getStart/elasticsearch-7.3.0/lib/* org.elasticsearch.bootstrap.Elasticsearch -d -p es.pid
...
```

{% endcode %}

&#x20; 위에서 es.pid 에 저장된 내용과 실행중인 프로세스 ID 모두 **39060**인 것을 확인할 수 있습니다. 프로세스 ID가 저장된 es.pid 파일은 실행 중인 Elasticsearch 프로세스를 종료하면 자동으로 삭제됩니다. 이제 이 옵션을 활용해서 Elasticsearch를 데몬으로 실행하는 start.sh 파일과 stop.sh 파일을 만들어 보겠습니다. 각 파일의 내용은 다음과 같이 합니다.

{% tabs %}
{% tab title="\<start.sh> 파일의 내용" %}

```
bin/elasticsearch -d -p es.pid
```

{% endtab %}

{% tab title="\<stop.sh> 파일의 내용" %}

```
kill `cat es.pid`
```

{% endtab %}
{% endtabs %}

&#x20; 위 파일들을 Elasticsearch 홈 경로에 저장하고 실행할 수 있도록 권한을 755로 변경합니다.

{% code title="start.sh, stop.sh 파일 생성 및 실행 권한 부여" %}

```bash
$ echo 'bin/elasticsearch -d -p es.pid' > start.sh

$ echo 'kill `cat es.pid`' > stop.sh

$ chmod 755 start.sh stop.sh
```

{% endcode %}

&#x20; 이제 start.sh를 실행해 Elasticsearch 프로세스가 실행된 것을 확인하고, 다시 stop.sh을 실행해 Elasticsearch 프로세스를 종료 해 보겠습니다.

{% code title="start.sh, stop.sh로 Elasticsearch 실행 및 종료" %}

```bash
$ ./start.sh

$ ps -ef | grep elasticsearch
  501 39259     1   0  8:18AM ttys000    0:09.14 /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/bin/java -Xms1g -Xmx1g -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -Des.networkaddress.cache.ttl=60 -Des.networkaddress.cache.negative.ttl=10 -XX:+AlwaysPreTouch -Xss1m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djna.nosys=true -XX:-OmitStackTraceInFastThrow -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true -Dio.netty.recycler.maxCapacityPerThread=0 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Djava.io.tmpdir=/var/folders/0d/m7m670h13pz3lvr9xjz07zk80000gn/T/elasticsearch-5569881625664576635 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=data -XX:ErrorFile=logs/hs_err_pid%p.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -Xloggc:logs/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=32 -XX:GCLogFileSize=64m -Dio.netty.allocator.type=unpooled -XX:MaxDirectMemorySize=536870912 -Des.path.home=/Users/kimjmin/elastic/getStart/elasticsearch-7.3.0 -Des.path.conf=/Users/kimjmin/elastic/getStart/elasticsearch-7.3.0/config -Des.distribution.flavor=default -Des.distribution.type=tar -Des.bundled_jdk=true -cp /Users/kimjmin/elastic/getStart/elasticsearch-7.3.0/lib/* org.elasticsearch.bootstrap.Elasticsearch -d -p es.pid
  501 39267 39259   0  8:18AM ttys000    0:00.02 /Users/kimjmin/elastic/getStart/elasticsearch-7.3.0/modules/x-pack-ml/platform/darwin-x86_64/bin/controller
  501 39269 38460   0  8:18AM ttys000    0:00.00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn elasticsearch

$ ls
LICENSE.txt    README.textile config         es.pid         lib            modules        start.sh
NOTICE.txt     bin            data           jdk            logs           plugins        stop.sh

$ ./stop.sh

$ ps -ef | grep elasticsearch
  501 39291 38460   0  8:19AM ttys000    0:00.00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn elasticsearch

$ ls
LICENSE.txt    README.textile config         jdk            logs           plugins        stop.sh
NOTICE.txt     bin            data           lib            modules        start.sh
```

{% endcode %}

**start.sh** 명령으로 Elasticsearch 가 백그라운드로 실행되었고, **ls** 명령으로 `es.pid` 파일이 생성된 것을 확인할 수 있습니다. 다시 **stop.sh** 명령으로 Elasticsearch 를 종료 한 뒤에는 `es.pid` 파일이 삭제된 것을 확인할 수 있습니다.


# 2.2.2 Unix RPM (yum) 설치 및 실행

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 레드햇 리눅스 계열에서 편하게 사용 가능한 yum 패키지로도 설치가 가능합니다. 버전에 따라 다를 수 있으므로 정확한 설정은 [공식 문서](<https://www.elastic.co/guide/en/elasticsearch/reference/current/rpm.html >) 에서 확인하시기 바랍니다.

### yum 다운로드

&#x20; 먼저 최신 버전의 elasticsearch를 yum 으로 설치하기 위해 /etc/yum.repos.d/ 디렉토리 아래에 elasticsearch.repo 파일을 만들고 아래와 같이 내용을 입력합니다.

{% code title="/etc/yum.repos.d/elasticsearch.repo" %}

```
[elasticsearch-7.x]
name=Elasticsearch repository for 7.x packages
baseurl=https://artifacts.elastic.co/packages/7.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=1
autorefresh=1
type=rpm-md
```

{% endcode %}

Apache 2.0 라이센스 배포판을 설치하려면 baseurl 부분을 아래와 같이 입력합니다.

{% code title="/etc/yum.repos.d/elasticsearch.repo" %}

```
baseurl=https://artifacts.elastic.co/packages/oss-7.x/yum
```

{% endcode %}

&#x20; 파일을 추가하고 나서 이제 yum명령을 이용해서 Elasticsearch를 설치합니다.

{% code title="yum 을 이용해서 Elasticsearch 설치" %}

```bash
$ sudo yum install elasticsearch -y
```

{% endcode %}

&#x20; 위와 같이 하면 최신 버전이 설치되고, 특정 버전을 설치하고 싶으면 다음과 같이 뒤에 버전을 명시 해 주면 됩니다.

{% code title="7.0.0 버전으로 설치" %}

```bash
$ sudo yum install elasticsearch-7.0.0 –y
```

{% endcode %}

### 서비스 등록

&#x20; ps -p 1 를 이용해서 SysV init 과 systemd 중 어떤 서비스를 사용하는지 확인합니다. init 을 사용하는 경우 서비스에 등록하기 위해 다음 명령을 실행합니다.

```bash
$ sudo chkconfig --add elasticsearch
```

&#x20; 이제 service 명령으로 elasticsearch의 실행 또는 종료가 가능합니다.

```bash
$ sudo -i service elasticsearch start
$ sudo -i service elasticsearch stop
```


# 2.2.3 윈도우 운영체제에서 MSI 파일로 설치

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.


# 2.3 elasticsearch 환경 설정

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Elasticsearch 는 각 노드들 별로 실행될 설정들을 적용함으로써 [노드들의 역할을 나누](/02-install/2.3-elasticsearch/2.3.3-node-settings)거나 클러스터의 속성을 결정하게 됩니다. Elasticsearch 의 실행 환경을 설정하는 방법은 크게 2가지가 있습니다. 홈 디렉토리의 config 경로 아래 있는 파일들을 변경하거나 시작 명령으로 설정하는 방법입니다. config 경로 아래의 파일들에서는 다음과 같은 설정들이 가능합니다.

* [jvm.options](/02-install/2.3-elasticsearch/2.3.1-jvm.options) - Java 힙메모리 및 환경변수&#x20;
* [elasticsearch.yml](/02-install/2.3-elasticsearch/2.3.2-elasticsearch.yml) - Elasticsearch 옵션&#x20;
* log4j2.properties - 로그 관련 옵션&#x20;

&#x20; 그 외에도 Elasticsearch 를 처음 실행할 때 [-E 커맨드](/02-install/2.3-elasticsearch/2.3.4-cofig-on-start-command) 라인 명령을 통해서도 가능합니다.


# 2.3.1 jvm.options

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Elasticsearch는 Java의 가상머신 위에서 실행이 되는데 7.0 기준으로 1gb의 힙메모리가 기본으로 설정되어 있습니다. 이 설정들은 jvm.options 파일에서 아래 내용들을 수정하여 변경할 수 있습니다.

{% code title="config/jvm.options" %}

```
-Xms1g
-Xmx1g
```

{% endcode %}

&#x20; 이 밖에도 Elasticsearch를 실행할 때 java와 관련된 환경변수들은 대부분 jvm.options 파일에서 설정이 가능합니다.


# 2.3.2 elasticsearch.yml

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; elasticsearch 실행 환경에 대한 실제 설정들은 대부분 elasticsearch.yml 파일에서 설정합니다. 확장자에서 보듯 YAML 문법으로 설정을 하기 때문에 옵션을 설정할 때는 들여쓰기에 유의해야 합니다. 예를 들어 아래의 두 방법들은 같은 설정을 나타냅니다.

{% code title="config/elasticsearch.yml" %}

```yaml
path:
    data: /var/lib/elasticsearch
    logs: /var/log/elasticsearch
```

{% endcode %}

{% code title="config/elasticsearch.yml" %}

```yaml
path.data: /var/lib/elasticsearch
path.logs: /var/log/elasticsearch
```

{% endcode %}

{% hint style="danger" %}
&#x20; 매 라인의 들여쓰기가 정확하지 않으면 같은 레벨에 설정되어야 할 값들이 하위 레벨 설정으로 들어가는 경우가 있습니다. 또&#xD55C;**`<key>: <value>`**&#xAC00;운데에 있는 콜&#xB860;**`:`**&#xACFC;**`<value>`**&#xAC12; 사이에는 반드시 공백이 있어야 하며 붙여쓰게 되면 오류가 발생하기 때문에 띄어쓰기에 항상 주의해야 합니다.
{% endhint %}

&#x20; elasticsearch.yml 에서 하는 주요 설정들은 다음과 같습니다. 각 설정 들의 실제 사용 예는 다음장에서 클러스터링을 설명하면서 더 자세히 다루도록 하겠습니다.

### cluster.name: "<클러스터명>"

&#x20; 클러스터명을 설정할 수 있습니다. Elasticsearch의 노드들은 클러스터명이 같으면 같은 클러스터로 묶이고 클러스터명이 다르면 동일한 물리적 장비나 바인딩이 가능한 네트워크상에 있더라도 서로 다른 클러스터로 바인딩이 됩니다. 디폴트 클러스터명은 "**elasticsearch**" 이며 충돌을 방지하기 위해 클러스터명은 반드시 고유한 이름으로 설정하도록 합니다.

### node.name: "<노드명>"

&#x20; 실행중인 각각의 elasticsearch 노드들을 구분할 수 있는 노드의 이름을 설정할 수 있습니다. 설정하지 않으면 노드명은 7.0 버전 부터는 호스트명, 6.x 이하 버전에서는 프로세스 UUID의 첫 7글자가 노드명으로 설정됩니다.

### node.attr.\<key>: "\<value>"

&#x20; 노드별로 속성을 부여하기 위한 일종의 네임스페이스를 지정합니다. 이 설정을 이용하면 hot / warm 아키텍쳐를 구성하거나 물리 장비 구성에 따라 샤드 배치를 임의적으로 조절하는 등의 설정이 가능합니다.

### path.data: \[ "<경로>" ]

&#x20; 색인된 데이터를 저장하는 경로를 지정합니다. 디폴트는 Elastcisearch가 설치된 홈 경로 아래의 data 디렉토리 입니다. 배열 형태로 여러개의 경로값의 입력이 가능하기 때문에 한 서버에서 디스크 여러개를 사용할 수 있습니다.

### path.logs: "<경로>"

&#x20; Elasticsearch 실행 로그를 저장하는 경로를 지정합니다. 디폴트는 Elastcisearch가 설치된 홈 경로 아래의 logs 디렉토리 입니다. 실행중인 시스템 로그는 `<클러스터명>.log` 형티의 파일로 저장되며 날짜가 변경되면 이전 로그 파일은 뒤에 날짜가 붙은 파일로 변경됩니다.

### bootstrap.memory\_lock: true

&#x20; Elasticsearch가 사용중인 힙메모리 영역을 다른 자바 프로그램이 간섭 못하도록 미리 점유하는 설정입니다 항상 true 로 놓고 사용하는것을 권장합니다.

### network.host: \<ip 주소>

&#x20; Elasticsearch가 실행되는 서버의 ip 주소입니다. 디폴트는 루프백(127.0.0.1) 입니다. 주석 처리 되어 있거나 루프백인 경우에는 Elasticsearch 노드가 개발 모드로 실행이 됩니다. 만약에 이 설정을 실제 IP 주소로 변경하게 되면 그 때부터는 운영 모드로 실행이 되며 노드를 시작할 때 부트스트랩 체크를 하게 됩니다. network.host는 서버의 내/외부 주소를 모두 지정하는데 만약 내부망에서 사용하는 주소와 외부망에서 접근하는 주소를 다르게 설정하고자 하면 아래의 값 들을 이용해서 구분이 가능합니다.

* `network.bind_host` : 내부망
* `network.publish_host` : 외부망

&#x20; 그리고 network.host 설정에 사용되는 특별한 변수값이 있는데 다음과 같습니다.&#x20;

* `_local_` : 루프백 주소 127.0.0.1 과 같습니다. 디폴트로 설정되어 있습니다.
* `_site_` : 로컬 네트워크 주소로 설정됩니다. 실제로 클러스터링 구성 시 주로 설정하는 값입니다.&#x20;
* `_global_` : 네트워크 외부에서 바라보는 주소로 설정합니다.

&#x20; 실제로 클러스터를 구성할 때 설정을 `network.host: _site_` 로 해 놓으면 서버의 네트워크 주소가 바뀌어도 설정 파일은 변경하지 않아도 되기 때문에 편리합니다.

### http.port: <포트 번호>

&#x20; Elasticsearch가 클라이언트와 통신하기 위한 http 포트를 설정합니다. 디폴트는 **9200** 이며, 포트가 이미 사용중인 경우 **9200 \~ 9299** 사이 값을 차례대로 사용합니다.

### transport.port: <포트 번호>

&#x20; Elasticsearch 노드들 끼리 서로 통신하기 위한 tcp 포트를 설정합니다. 디폴트는 **9300** 이며, 포트가 이미 사용중인 경우 **9300 \~ 9399** 사이의 값을 차례대로 사용합니다.

### discovery.seed\_hosts: \[ "<호스트-1>", "<호스트-2>", ... ]

&#x20; 클러스터 구성을 위해 바인딩 할 원격 노드의 IP 또는 도메인 주소를 배열 형태로 입력합니다. 주소만 적는 경우 디폴트로 9300\~9305 사이의 포트값을 검색하며, tcp 포트가 이 범위 밖에 설정 된 경우 포트번호 까지 같이 적어주어야 합니다. 이렇게 원격에 있는 노드들을 찾아 바인딩 하는 과정을 **디스커버리** 라고 합니다. 디스커버리에 대해서는 [3.1 클러스터 구성 : 디스커버리](/03-cluster/3.1-cluster-settings#discovery) 부분에서 설명하고 있습니다.

{% hint style="warning" %}
**discovery.seed\_hosts** 옵션은 7.0 부터 추가된 기능입니다. 6.x 이전 버전에서는 대신에 젠 디스커버리를 사용했습니다. 사용 방법은 아래와 같습니다.

**discovery.zen.ping.unicast.hosts: \[ "<호스트-1>", "<호스트-2>", ... ]**
{% endhint %}

### cluster.initial\_master\_nodes: \[ "<노드-1>", "<노드-2>" ]

&#x20; 클러스터가 최초 실행 될 때 명시된 노드들을 대상으로 마스터 노드를 선출합니다. 마스터 노드에 대해서는 [3.3 마스터 노드와 데이터 노드](/03-cluster/3.3-master-and-data-nodes) 장에서 자세 다루도록 하겠습니다.

{% hint style="warning" %}
**cluster.initial\_master\_nodes** 옵션 역시 7.0 부터 추가된 기능입니다. 6.x 이전 버전에서는 최소 마스터 후보 노드를 지정하기 위해 다음 옵션을 사용했습니다. 7.0 버전 부터는 최소 마스터 후보 노드의 크기가 능동적으로 변경됩니다.

**discovery.zen.minimum\_master\_nodes: 2**
{% endhint %}

&#x20; 노드 실행시 지정된 환경변수를 elasticsearch.yml 에서 ${환경변수명} 형식으로 사용이 가능합니다.

{% code title="config/elasticsearch.yml 에서 환경변수 사용" %}

```yaml
node.name: ${HOSTNAME}
network.host: ${ES_NETWORK_HOST}
```

{% endcode %}


# 2.3.3 노드의 역할 : master, data, ingest, ml

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Elasticsearch의 노드는 수행하는 다양한 역할들이 있는데, 각자의 노드들이 서로 다른 역할들을 수행하도록 클러스터를 구성할 수 있습니다. 아래 설정들의 모든 디폴트 값은 **true** 이며 기본적으로 노드는 명시된 모든 역할들을 수행합니다. 특정 값 들을 false 로 설정 함으로서 노드의 역할들을 구분지어 클러스터를 구성할 수 있습니다.

### node.master: true

&#x20; 마스터 후보(master eligible) 노드 여부를 설정합니다. false인 경우 이 노드는 마스터 노드로 선출이 불가능합니다. 모든 클러스터는 1개의 마스터 노드가 존재하며 마스터 노드가 다운되거나 끊어진 경우 남은 마스터 후보 노드들 중에서 새로운 마스터 노드가 선출되게 됩니다.

### node.data: true

&#x20;  노드가 데이터를 저장하도록 합니다. false인 경우 이 노드는 데이터를 저장하지 않습니다.

### node.ingest: true

&#x20; 데이터 색인시 전처리 작업인 ingest pipleline 작업의 수행을 할 수 있는지 여부를 지정합니다. false인 경우 이 노드에서는 ingest pipeline 작업의 실행이 불가능합니다.

### node.ml: true

&#x20; 이 노드가 머신러닝 작업 수행을 할 수 있는지 여부를 지정합니다. false 인 경우 이 노드애서는 머신러닝 작업이 수행되지 않습니다.

&#x20; 예를 들어 클러스터에서 어떤 노드를 데이터는 저장하거나 색인하지 않고 오직 클러스터 상태를 관리하는 마스터 노드의 역할만 수행하도록 설정하려면 아래와 같이 설정합니다.

{% code title="config/elasticsearch.yml - 전용 마스터 노드 설정" %}

```yaml
node.master: true
node.data: false
node.ingest: false
node.ml: false
```

{% endcode %}

&#x20; 이런 방법으로 클러스터 안의 노드들을 마스터 전용 노드, 데이터 전용 노드 등으로 분리하여 유연한 구성을 할 수 있습니다.

{% hint style="info" %}
앞의 모든 설정을 **false** 로 하게 되면 해당 노드는 데이터를 저장하거나 색인하지 않고 클러스터 상태를 업데이트도 하지 않으며 오직 클라이언트와 통신만 하는 역할로 사용이 가능합니다. 이런 노드를 코디네이트 온리 노드 (coordinate only node) 라고 부릅니다.
{% endhint %}


# 2.3.4 커맨드 라인 설정

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; elasticsearch.yml 파일에 설정하는 것 외에도 Elasticsearch 실행 시 커맨드 명령에 `-E <옵션>=<값>` 을 이용해서 환경 설정이 가능합니다. 예를 들어 클러스터명은 **my-cluster** 노드명은 **node-1**로 노드를 실행하기 위해서는 다음과 같이 실행합니.

{% code title="클러스터명: my-cluster / 노드명:  node-1 로 노드 실행" %}

```bash
$ bin/elasticsearch -E cluster.name=my-cluster -E node.name="node-1"
[2019-08-26T14:23:51,399][INFO ][o.e.e.NodeEnvironment    ] [node-1] using [1] data paths, mounts [[/ (/dev/disk1s1)]], net usable_space [88.9gb], net total_space [465.6gb], types [apfs]
[2019-08-26T14:23:51,401][INFO ][o.e.e.NodeEnvironment    ] [node-1] heap size [989.8mb], compressed ordinary object pointers [true]
[2019-08-26T14:23:51,455][INFO ][o.e.n.Node               ] [node-1] node name [node-1], node ID [RDBLYDInSxmMV1PEVit_pQ], cluster name [my-cluster]
[2019-08-26T14:23:51,455][INFO ][o.e.n.Node               ] [node-1] version[7.3.0], pid[50389], build[default/tar/de777fa/2019-07-24T18:30:11.767338Z], OS[Mac OS X/10.14.6/x86_64], JVM[Oracle Corporation/Java HotSpot(TM) 64-Bit Server VM/1.8.0_151/25.151-b12]
[2019-08-26T14:23:51,456][INFO ][o.e.n.Node               ] [node-1] JVM home [/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre]
[2019-08-26T14:23:51,456][INFO ][o.e.n.Node               ] [node-1] JVM arguments [-Xms1g, -Xmx1g, -XX:+UseConcMarkSweepGC, -XX:CMSInitiatingOccupancyFraction=75, -XX:+UseCMSInitiatingOccupancyOnly, -Des.networkaddress.cache.ttl=60, -Des.networkaddress.cache.negative.ttl=10, -XX:+AlwaysPreTouch, -Xss1m, -Djava.awt.headless=true, -Dfile.encoding=UTF-8, -Djna.nosys=true, -XX:-OmitStackTraceInFastThrow, -Dio.netty.noUnsafe=true, -Dio.netty.noKeySetOptimization=true, -Dio.netty.recycler.maxCapacityPerThread=0, -Dlog4j.shutdownHookEnabled=false, -Dlog4j2.disable.jmx=true, -Djava.io.tmpdir=/var/folders/0d/m7m670h13pz3lvr9xjz07zk80000gn/T/elasticsearch-5549928559955731670, -XX:+HeapDumpOnOutOfMemoryError, -XX:HeapDumpPath=data, -XX:ErrorFile=logs/hs_err_pid%p.log, -XX:+PrintGCDetails, -XX:+PrintGCDateStamps, -XX:+PrintTenuringDistribution, -XX:+PrintGCApplicationStoppedTime, -Xloggc:logs/gc.log, -XX:+UseGCLogFileRotation, -XX:NumberOfGCLogFiles=32, -XX:GCLogFileSize=64m, -Dio.netty.allocator.type=unpooled, -XX:MaxDirectMemorySize=536870912, -Des.path.home=/Users/kimjmin/elastic/getStart/elasticsearch-7.3.0, -Des.path.conf=/Users/kimjmin/elastic/getStart/elasticsearch-7.3.0/config, -Des.distribution.flavor=default, -Des.distribution.type=tar, -Des.bundled_jdk=true]
```

{% endcode %}

{% hint style="warning" %}
환경 설정이 elasticsearch.yml 과 커맨드 명령 -E 에 모두 있는 경우에는 -E 커맨드 명령에서 한 설정이 더 우선해서 적용이 됩니다.
{% endhint %}

&#x20; 지금까지 기본적인 설치 방법과 설정 정보들을 살펴보았습니다. 지금까지 다룬 설정들 외에도 추가적으로 다양한 설정들이 있습니다. 설치와 설정에 대한 더 많은 내용들은 아키텍쳐 구성과 예제 부분에서 더 자세히 다루도록 하겠습니다.


# 3. Elasticsearch 시스템 구조

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Elasticsearch는 대용량 데이터의 증가에 따른 스케일 아웃과 데이터 무결성을 유지하기 위한 클러스터링을 지원합니다. 항상 클러스터를 기본으로 동작을 하며 1개의 노드만 있어도 클러스터로 구성이 됩니다.


# 3.1 클러스터 구성

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

### 여러 서버에 하나의 클러스터로 실행&#x20;

&#x20; Elasticsearch의 노드들은 클라이언트와의 통신을 위한 http 포트(9200\~9299), 노드 간의 데이터 교환을 위한 tcp 포트 (9300\~9399) 총 2개의 네트워크 통신을 열어두고 있습니다. 일반적으로 1개의 물리 서버마다 하나의 노드를 실행하는 것을 권장하고 있습니다. 3개의 다른 물리 서버에서 각각 1개 씩의 노드를 실행하면 각 클러스터는 다음과 같이 구성됩니다.

![](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUvu8iUqn07h_vY2sX%2F-LnEXJ5Avdp4mVjPeH2b%2Fimage.png?alt=media\&token=447df06c-a4d5-4330-81cb-2e71804db02a)

&#x20; 하나의 물리적인 서버 안에서 여러 개의 노드를 실행하는 것도 가능합니다. 이 경우에는 각 노드들은 차례대로 9200, 9201… 순으로 포트를 사용하게 됩니다. 클라이언트는 9200, 9201 등의 포트를 통해 원하는 노드와 통신을 할 수 있습니다. 만약에 서버1 에서 두개의 노드를 실행하고, 또 다른 서버에서 한개의 노드를 실행시키면 클러스터는 다음과 같이 구성됩니다.

![](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUvu8iUqn07h_vY2sX%2F-LnEXWfNEyRQtfSEdK7v%2Fimage.png?alt=media\&token=50451080-1637-4b44-a687-6de356a67115)

&#x20; 서버1에는 두개의 노드가 있기 때문에 서버 1의 두번째 노드는 실행되는 http, tcp 포트가 각각 9201, 9301 로 실행이 됩니다.

&#x20; 물리적인 구성과 상관 없이 여러 노드가 하나의 클러스터로 묶이기 위해서는 클러스터명 `cluster.name` 설정이 묶여질 노드들 모두 동일해야 합니다. 같은 서버나 네트워크망 내부에 있다 하더라도 `cluster.name`이 동일하지 않으면 논리적으로 서로 다른 클러스터로 실행이 되고, 각각 별개의 시스템으로 인식이 됩니다.

### 하나의 서버에서 여러 클러스터 실행&#x20;

&#x20; 하나의 물리 서버에 3개의 노드를 실행시킨다고 가정 해 보겠습니다. 노드들의 이름은 각각 **node-1**, **node-2**, **node-3** 이고 node-1과 node-2의 클러스터명은 **es-cluster-1**, node-3의 클러스터명은 **es-cluster-2**로 실행을 합니다. 설정은 config/elasticsearch.yml 파일에서 아래와 같이 입력합니다. 각 탭에서 노드의 설정을 확인합니다.

{% tabs %}
{% tab title="node-1" %}
{% code title="config/elasticsearch.yml" %}

```yaml
cluster.name: es-cluster-1
node.name: "node-1"
```

{% endcode %}
{% endtab %}

{% tab title="node-2" %}
{% code title="config/elasticsearch.yml" %}

```yaml
cluster.name: es-cluster-1
node.name: "node-2"
```

{% endcode %}
{% endtab %}

{% tab title="node-3" %}
{% code title="config/elasticsearch.yml" %}

```yaml
cluster.name: es-cluster-2
node.name: "node-3"
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 또는 다음과 같이 실행 커맨드를 이용해서도 설정이 가능합니다.

{% tabs %}
{% tab title="node-1" %}
{% code title="" %}

```bash
$ bin/elasticsearch -Ecluster.name=es-cluster-1 -Enode.name=node-1
```

{% endcode %}
{% endtab %}

{% tab title="node-2" %}
{% code title="" %}

```bash
$ bin/elasticsearch -Ecluster.name=es-cluster-1 -Enode.name=node-2
```

{% endcode %}
{% endtab %}

{% tab title="node-3" %}
{% code title="" %}

```bash
$ bin/elasticsearch -Ecluster.name=es-cluster-2 -Enode.name=node-3
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; node-1과 node-2는 하나의 클러스터로 묶여있기 때문에 데이터 교환이 일어납니다. node-1로 입력된 데이터는 node-2 에서도 읽을 수 있으며 그 반대도 가능합니다. 하지만 node-3 은 클러스터가 다르기 때문에 node-1, node-2에 입력된 데이터를 node-3 에서 읽을 수는 없습니다.

&#x20; 가장 먼저 node-1을 실행시켰을 때 나타나는 화면입니다.

{% code title="node-1 실행" %}

```bash
$ bin/elasticsearch -Ecluster.name=es-cluster-1 -Enode.name=node-1
[2019-08-27T05:22:07,254][INFO ][o.e.e.NodeEnvironment    ] [node-1] using [1] data paths, mounts [[/ (/dev/disk1s1)]], net usable_space [88.2gb], net total_space [465.6gb], types [apfs]
[2019-08-27T05:22:07,256][INFO ][o.e.e.NodeEnvironment    ] [node-1] heap size [989.8mb], compressed ordinary object pointers [true]
[2019-08-27T05:22:07,261][INFO ][o.e.n.Node               ] [node-1] node name [node-1], node ID [RYhEbLLjQKaoHzGNkSNo-g], cluster name [es-cluster-1]
...
```

{% endcode %}

&#x20; 위 실행 화면에서 먼저 노드명이 `[node-1]`인 것을 확인할 수 있습니다. 실행 메시지를 계속 확인 하다 보면 다음과 같은 부분이 있습니다.

{% code title="node-1 실행 화면" %}

```bash
[2019-08-27T05:22:14,030][INFO ][o.e.t.TransportService   ] [node-1] publish_address {127.0.0.1:9300}, bound_addresses {[::1]:9300}, {127.0.0.1:9300}
...
[2019-08-27T05:22:17,307][INFO ][o.e.h.AbstractHttpServerTransport] [node-1] publish_address {127.0.0.1:9200}, bound_addresses {[::1]:9200}, {127.0.0.1:9200}
```

{% endcode %}

&#x20; `[o.e.t.TransportService]` 에서 tcp 포트 **9300**, 그리고 `[o.e.h.AbstractHttpServerTransport]` 에서 http 포트 **9200**을 각각 확인할 수 있습니다.

&#x20; 모든 클러스터에는 반드시 하나의 마스터 노드가 존재합니다. 실행 메시지의 다음 부분을 살펴보겠습니다.

{% code title="node-1 실행 화면" %}

```bash
[2019-08-27T05:22:17,230][INFO ][o.e.c.s.MasterService    ] [node-1] elected-as-master ([1] nodes joined)[{node-1}{RYhEbLLjQKaoHzGNkSNo-g}{KZxmWwFVTPKU5QHDbmULfg}{127.0.0.1}{127.0.0.1:9300}{dim}{ml.machine_memory=17179869184, xpack.installed=true, ml.max_open_jobs=20} elect leader, _BECOME_MASTER_TASK_, _FINISH_ELECTION_], term: 1, version: 1, reason: master node changed {previous [], current [{node-1}{RYhEbLLjQKaoHzGNkSNo-g}{KZxmWwFVTPKU5QHDbmULfg}{127.0.0.1}{127.0.0.1:9300}{dim}{ml.machine_memory=17179869184, xpack.installed=true, ml.max_open_jobs=20}]}
...
```

{% endcode %}

`[o.e.c.s.MasterService] [node-1] elected-as-master`부분에서 현재 node-1 이 마스터 노드로 선출 것을 확인할 수 있습니다. 마스터 노드에 대한 자세한 설명은 뒤에서 계속 다루도록 하겠습니다.&#x20;

&#x20; 이제 node-1이 실행중인 상태에서 두번째 노드인 node-2를 실행시키면 다음과 같은 실행 메시지들을 확인할 수 있습니다.

{% code title="node-2 실행" %}

```bash
$ bin/elasticsearch -Ecluster.name=es-cluster-1 -Enode.name=node-2
...
[2019-08-27T07:50:54,308][INFO ][o.e.n.Node               ] [node-2] starting ...
[2019-08-27T07:50:54,479][INFO ][o.e.t.TransportService   ] [node-2] publish_address {127.0.0.1:9301}, bound_addresses {[::1]:9301}, {127.0.0.1:9301}
...
[2019-08-27T07:50:54,488][WARN ][o.e.b.BootstrapChecks    ] [node-2] the default discovery settings are unsuitable for production use; at least one of [discovery.seed_hosts, discovery.seed_providers, cluster.initial_master_nodes] must be configured
[2019-08-27T07:50:54,503][INFO ][o.e.c.c.ClusterBootstrapService] [node-2] no discovery configuration found, will perform best-effort cluster bootstrapping after [3s] unless existing master is discovered
[2019-08-27T07:50:54,778][INFO ][o.e.c.s.ClusterApplierService] [node-2] master node changed {previous [], current [{node-1}{RYhEbLLjQKaoHzGNkSNo-g}{KZxmWwFVTPKU5QHDbmULfg}{127.0.0.1}{127.0.0.1:9300}{dim}{ml.machine_memory=17179869184, ml.max_open_jobs=20, xpack.installed=true}]}, added {{node-1}{RYhEbLLjQKaoHzGNkSNo-g}{KZxmWwFVTPKU5QHDbmULfg}{127.0.0.1}{127.0.0.1:9300}{dim}{ml.machine_memory=17179869184, ml.max_open_jobs=20, xpack.installed=true},}, term: 1, version: 16, reason: ApplyCommitRequest{term=1, version=16, sourceNode={node-1}{RYhEbLLjQKaoHzGNkSNo-g}{KZxmWwFVTPKU5QHDbmULfg}{127.0.0.1}{127.0.0.1:9300}{dim}{ml.machine_memory=17179869184, ml.max_open_jobs=20, xpack.installed=true}}
...
[2019-08-27T07:50:55,216][INFO ][o.e.h.AbstractHttpServerTransport] [node-2] publish_address {127.0.0.1:9201}, bound_addresses {[::1]:9201}, {127.0.0.1:9201}
...
```

{% endcode %}

&#x20; node-2의 경우 tcp 포트가 **9301**, http 포트가 **9201** 로 잡힌 것을 확인할 수 있습니다.&#x20;

&#x20; `[o.e.c.s.ClusterApplierService] [node-2] master node changed {previous [], current [{node-1} ... added {{node-1} ...` 부분을 보면 같은 클러스터에 이미 실행중인 마스터 노드 **node-1**이 있기 때문에 node-2 는 node-1이 마스터로 있는 클러스터에 묶인 것이 확인됩니다.

&#x20; node-2 를 실행하고 난 뒤 다시 **node-1** 의 콘솔 메시지를 확인하면 마찬가지로 node-2 가 클러스터에 추가되었다는 메시지를 확인할 수 있습니다.

{% code title="node-1 실행 화면" %}

```bash
[2019-08-27T07:50:54,736][INFO ][o.e.c.s.MasterService    ] [node-1] node-join[{node-2}{1EQ3a93iRMqppD49aQoTzg}{4CRm0xbHT36e38r2udKx0g}{127.0.0.1}{127.0.0.1:9301}{dim}{ml.machine_memory=17179869184, ml.max_open_jobs=20, xpack.installed=true} join existing leader], term: 1, version: 16, reason: added {{node-2}{1EQ3a93iRMqppD49aQoTzg}{4CRm0xbHT36e38r2udKx0g}{127.0.0.1}{127.0.0.1:9301}{dim}{ml.machine_memory=17179869184, ml.max_open_jobs=20, xpack.installed=true},}
[2019-08-27T07:50:55,200][INFO ][o.e.c.s.ClusterApplierService] [node-1] added {{node-2}{1EQ3a93iRMqppD49aQoTzg}{4CRm0xbHT36e38r2udKx0g}{127.0.0.1}{127.0.0.1:9301}{dim}{ml.machine_memory=17179869184, ml.max_open_jobs=20, xpack.installed=true},}, term: 1, version: 16, reason: Publication{term=1, version=16}
```

{% endcode %}

&#x20; 이제 node-1, node-2 가 실행 중인 상태에서 node-3 을 추가로 실행 해 보겠습니다.

{% code title="node-3 실행 화면" %}

```bash
$ bin/elasticsearch -Ecluster.name=es-cluster-2 -Enode.name=node-3
...
[2019-08-27T08:01:37,474][INFO ][o.e.t.TransportService   ] [node-3] publish_address {127.0.0.1:9302}, bound_addresses {[::1]:9302}, {127.0.0.1:9302}
...
[2019-08-27T08:01:37,640][WARN ][o.e.d.HandshakingTransportAddressConnector] [node-3] handshake failed for [connectToRemoteMasterNode[[::1]:9300]]
...
[2019-08-27T08:01:40,671][INFO ][o.e.c.s.MasterService    ] [node-3] elected-as-master ([1] nodes joined)[{node-3}{XPFkVAjKQfaVoWkd4Hqv5A}{8Y3wZO41R_CmlMV-JJhoPg}{127.0.0.1}{127.0.0.1:9302}{dim}{ml.machine_memory=17179869184, xpack.installed=true, ml.max_open_jobs=20} elect leader, _BECOME_MASTER_TASK_, _FINISH_ELECTION_], term: 1, version: 1, reason: master node changed {previous [], current [{node-3}{XPFkVAjKQfaVoWkd4Hqv5A}{8Y3wZO41R_CmlMV-JJhoPg}{127.0.0.1}{127.0.0.1:9302}{dim}{ml.machine_memory=17179869184, xpack.installed=true, ml.max_open_jobs=20}]}
...
[2019-08-27T08:01:40,797][INFO ][o.e.h.AbstractHttpServerTransport] [node-3] publish_address {127.0.0.1:9202}, bound_addresses {[::1]:9202}, {127.0.0.1:9202}
```

{% endcode %}

&#x20; 먼저 node-3 의 http, tcp 포트 설정은 **9202**, **9302** 를 사용하도록 설정되었습니다. \
`[o.e.d.HandshakingTransportAddressConnector] [node-3] ... handshake failed for ...`\
부분을 보면 같은 서버에서 실행중인 node-1, node-2 를 찾았지만 클러스터명이 `es-cluster-2` 로 다르기 때문에 node-3은 node-1, node-2 와 같은 클러스터로 바인딩 되지 않았습니다. 그리고 `[o.e.c.s.MasterService ] [node-3] elected-as-master ...` 부분에서 **node-3** 스스로 `es-cluster-2` 클러스터의 마스터 노드로 선출 된 것을 확인할 수 있습니다.

&#x20; 지금까지 실행한 노드들의 클러스터 구조를 그림으로 나타내면 아래 그림과 같습니다.

![하나의 물리 서버에서 서로 다른 두 개의 클러스터 실행](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUvu8iUqn07h_vY2sX%2F-LnFBFFyMu6dOc9_OQ1s%2Fimage.png?alt=media\&token=88af6fac-8297-4c79-a3e1-3f8779214559)

### 디스커버리 (Discovery)

&#x20; 노드가 처음 실행 될 때 같은 서버, 또는 `discovery.seed_hosts: [ ]` 에 설정된 네트워크 상의 다른 노드들을 찾아 하나의 클러스터로 바인딩 하는 과정을 **디스커버리** 라고 합니다. 디스커버리는 다음과 같은 순서로 이루어집니다.

1. `discovery.seed_hosts` 설정에 있는 주소 순서대로 노드가 있는지 여부를 확인
   * 노드가 존재하는 경우 > `cluster.name` 확인
     * 일치하는 경우 > 같은 클러스터로 바인딩 > 종료
     * 일치하지 않는 경우 > 1로 돌아가서 다음 주소 확인 반복
   * 노드가 존재하지 않는 경우 > 1로 돌아가서 다음 주소 확인 반복
2. 주소가 끝날 때 까지 노드를 찾지 못한 경우
   * 스스로 새로운 클러스터 시작

![디스커버리 과정](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnVEF1ED-x6P43UYEm-%2F-LnKbiLZhJ4PDthfVOko%2Fimage.png?alt=media\&token=989ba2aa-5129-43db-b746-6738b6a121c1)

{% hint style="warning" %}
클러스터에 노드가 무수히 많아도 보통 `discovery.seed_hosts` 설정에는 처음에 탐색할 노드 3\~5 개 정도만 설정 하면 큰 문제 없이 클러스터가 바인딩 됩니다. 보통은 마스터 후보 노드들을 지정하게 되며 처음 탐색하는 대상 노드는 반드시 먼저 가동중이어야 합니다.
{% endhint %}


# 3.2 인덱스와 샤드 - Index & Shards

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Elasticsearch 에서는 단일 데이터 단위를 **도큐먼트(document)** 라고 하며 이 도큐먼트를 모아놓은 집합을 **인덱스(Index)** 라고 합니다. 인덱스라는 단어가 여러 뜻으로 사용되기 때문에 데이터 저장 단위인 인덱스는 **인디시즈(indices)** 라고 표현하기도 합니다. 이 책에서는 데이터를 Elasticsearch에 저장하는 행위는 **색인**, 그리고 도큐먼트의 집합 단위는 **인덱스** 라고 하겠습니다.

&#x20;  인덱스는 기본적으로 **샤드(shard)**&#xB77C;는 단위로 분리되고 각 노드에 분산되어 저장이 됩니다. 샤드는 루씬의 단일 검색 인스턴스 입니다. 다음은 하나의 인덱스가 5개의 샤드로 저장되도록 설정한 예 입니다.

![](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUxLhQtL_hJwYydM_5%2F-LnKhezUmymEBwLJzfzt%2Fimage.png?alt=media\&token=7bf94c91-342c-45c3-9e52-2320e19c667c)

### 프라이머리 샤드(Primary Shard)와 복제본(Replica)

&#x20; 인덱스를 생성할 때 별도의 설정을 하지 않으면 **7.0** 버전부터는 **디폴트로 1**개의 샤드로 인덱스가 구성되며 **6.x** 이하 버전에서는 **5개**로 구성됩니다. 클러스터에 노드를 추가하게 되면 샤드들이 각 노드들로 분산되고 디폴트로 1개의 복제본을 생성합니다. 처음 생성된 샤드를 **프라이머리 샤드(Primary Shard)**, 복제본은 **리플리카(Replica)** 라고 부릅니다. 예를 들어 한 인덱스가 5개의 샤드로 구성어 있고, 클러스터가 4개의 노드로 구성되어 있다고 가정하면 각각 5개의 프라이머리 샤드와 복제본, 총 10개의 샤드들이 전체 노드에 골고루 분배되어 저장됩니다.

![5개의 프라이머리 샤드와 복제본이 4개의 노드에 분산되어 저장된 예](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUxLhQtL_hJwYydM_5%2F-LnKhmx31SnLDNMKr_YT%2Fimage.png?alt=media\&token=fc99a84d-5f9f-4d3d-aad6-21e503567b33)

{% hint style="danger" %}
노드가 1개만 있는 경우 프라이머리 샤드만 존재하고 복제본은 생성되지 않습니다. Elasticsearch 는 아무리 작은 클러스터라도 데이터 가용성과 무결성을 위해 최소 3개의 노드로 구성 할 것을 권장하고 있습니.
{% endhint %}

&#x20; 같은 샤드와 복제본은 동일한 데이터를 담고 있으며 **반드시 서로 다른 노드에 저장이 됩니다**. 만약에 위 그림에서 Node-3 노드가 시스템 다운이나 네트워크 단절등으로 사라지면 이 클러스터는 Node-3 에 있던 0번과 4번 샤드들을 유실하게 됩니다. 하지만 아직 다른 노드들 Node-1, Node-2 에 0번, 4번 샤드가 남아있으므로 여전히 전체 데이터는 유실이 없이 사용이 가능합니다.

![Node-3 노드가 유실되어 0번, 4번 샤드가 다른 노드에 복제본을 새로 생성한 예](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUxLhQtL_hJwYydM_5%2F-LnKhur7V1PE9tkQF2XA%2Fimage.png?alt=media\&token=1bef4755-4ca1-44a7-932c-fe0556507ad5)

&#x20; 처음에 클러스터는 먼저 유실된 노드가 복구 되기를 기다립니다. 하지만 타임아웃이 지나 더 유실된 노드가 복구되지 않는다고 판단이 되면 Elasticsearch는 복제본이 사라져 1개만 남은 0번, 4번 샤드들의 복제를 시작합니다. 처음에 4개였던 노드가 3개로 줄어도 복제가 끝나면 0\~4번 까지의 프라이머리 샤드, 복제본이 각각 5개씩 총 10개의 데이터로 유지됩니다.

![노드가 3개로 줄었을 때도 전체 데이터 유지](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUxLhQtL_hJwYydM_5%2F-LnKiH_CNaz3XJakPUpk%2Fimage.png?alt=media\&token=3f6bdd15-4b78-444a-9e37-0a2c011fe338)

&#x20; 이렇게 프라이머리 샤드와 리플리카를 통해 Elasticsearch는 운영 중에 노드가 유실 되어도 데이터를 잃어버리지 않고 데이터의 가용성과 무결성을 보장합니다.

{% hint style="warning" %}
프라이머리 샤드가 유실된 경우에는 새로 프라이머리 샤드가 생성되는 것이 아니라, 남아있던 복제본이 먼저 프라이머리 샤드로 승격이 되고 다른 노드에 새로 복제본을 생성하게 됩니다.
{% endhint %}

### 샤드 개수 설정

&#x20; 샤드의 개수는 인덱스를 처음 생성할 때 지정할 수 있습니다. 프라이머리 샤드 수는 인덱스를 처음 생성할 때 지정하며, **인덱스를 재색인 하지 않는 이상 바꿀 수 없습니다**. 복제본의 개수는 나중에 변경이 가능합니다. 아래는 curl 명령을 통해 REST API로 샤드가 5개, 복제본은 1개인 books 라는 이름의 인덱스를 생성하는 예제입니다. REST API에 대해서는 다음 장에서 더 자세히 다루도록 하겠습니다.

{% code title="프라이머리 샤드 5, 복제본 1 인 books 인덱스 생성" %}

```bash
$ curl -XPUT "http://localhost:9200/books" -H 'Content-Type: application/json' -d'
{
  "settings": {
    "number_of_shards": 5,
    "number_of_replicas": 1
  }
}'

```

{% endcode %}

&#x20; books 인덱스의 복제본 수를 0으로 변경하려면 아래 명령으로 업데이트가 가능합니다.

{% code title="books 인덱스의 복제본 개수를 0 으로 변경" %}

```bash
$ curl -XPUT "http://localhost:9200/books/_settings" -H 'Content-Type: application/json' -d'
{
  "number_of_replicas": 0
}'
```

{% endcode %}

&#x20; 만약에 4개의 노드를 가진 클러스터에 프라이머리 샤드 5개, 복제본 1개인 books 인덱스, 그리고 프라이머리 샤드 3개 복제본 0개인 magazines 인덱스가 있다고 하면 전체 샤드들은 아래와 같은 모양으로 배치될 수 있습니다.

![books 인덱스와 magazines 인덱스](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUxLhQtL_hJwYydM_5%2F-LnKl_15PVn3pJ-V-jG6%2Fimage.png?alt=media\&token=a3fdac30-9030-44f8-b2a1-08811e09b2d9)


# 3.3 마스터 노드와 데이터 노드 - Master & Data Nodes

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

### 마스터 노드 (Master Node)

&#x20; Elasticsearch 클러스터는 하나 이상의 노드들로 이루어집니다. 이 중 하나의 노드는 인덱스의 메타 데이터, 샤드의 위치와 같은 클러스터 상태(Cluster Status) 정보를 관리하는 **마스터 노드**의 역할을 수행합니다. 클러스터마다 하나의 마스터 노드가 존재하며 마스터 노드의 역할을 수행할 수 있는 노드가 없다면 클러스터는 작동이 정지됩니다.

&#x20; `elasticsearch.yml`에 디폴트 설정은 node.master: true로 되어 있습니다. 기본적으로는 모든 노드가 마스터 노드로 선출될 수 있는 **마스터 후보 노드 (master eligible node)** 입니다. 만약에 현재 마스터 역할을 수행하고 있는 노드가 네트워크상에서 끊어지거나 다운되면 다른 마스터 후보 노드 중 하나가 마스터 노드로 선출이 되어 마스터 노드의 역할을 대신 수행하게 됩니다. 마스터 후보 노드들은 처음부터 마스터 노드의 정보들을 공유하고 있기 때문에 즉시 마스터 역할의 수행이 가능합니다.

&#x20; 클러스터가 커져서 노드와 샤드들의 개수가 많아지게 되면 모든 노드들이 마스터 노드의 정보를 계속 공유하는 것은 부담이 될 수 있습니다. 이때는 마스터 노드의 역할을 수행 할 후보 노드들만 따로 설정해서 유지하는 것이 전체 클러스터 성능에 도움이 될 수 있습니다. 마스터 노드로 사용하지 않는 노드들은 설정값을 `node.master: false` 로 하여 마스터 노드의 역할을 하지 않도록 합니다.

### 데이터 노드 (Data Node)

&#x20; 데이터 노드는 실제로 색인된 데이터를 저장하고 있는 노드입니다. 클러스터에서 마스터 노드와 데이터 노드를 분리하여 설정 할 때 마스터 후보 노드들은 `node.data: false` 로 설정하여 마스터 노드 역할만 하고 데이터는 저장하지 않도록 할 수 있습니다. 이렇게 하면 마스터 노드는 데이터는 저장하지 않고 클러스터 관리만 하게 되고, 데이터 노드는 클러스터 관리 작업으로 부터 자유롭게 되어 데이터 처리에만 집중할 수 있습니다.

&#x20; 다음은 4개의 노드를 실행하는데 node-1 은 마스터의 역할만 실행하는 전용 노드 (영어로는 Dedicated Master Node 라고 부릅니다), node-2, node-3, node-4 는 마스터 역할은 하지 않고 데이터 저장만 하는 노드로 설정하는 예제입니다.

{% tabs %}
{% tab title="Node-1 (마스터)" %}
{% code title="config/elasticsearch.yml" %}

```yaml
node.master: true
node.data: false
```

{% endcode %}
{% endtab %}

{% tab title="Node-2 (데이터)" %}
{% code title="config/elasticsearch.yml" %}

```yaml
node.master: false
node.data: true
```

{% endcode %}
{% endtab %}

{% tab title="Node-3 (데이터)" %}
{% code title="config/elasticsearch.yml" %}

```yaml
node.master: false
node.data: true
```

{% endcode %}
{% endtab %}

{% tab title="Node-4 (데이터)" %}
{% code title="config/elasticsearch.yml" %}

```yaml
node.master: false
node.data: true
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 위와 같이 설정한 4개의 노드를 하나의 클러스터로 묶고 데이터를 입력하게 되면 데이터는 다음과 같이 node-2, node-3, node-4 에만 저장이 됩니다.

![마스터 전용 노드와 데이터 노드 구분](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUyizASxjZepqPV1G1%2F-LnKpwBWIwwxZXr1cEMp%2Fimage.png?alt=media\&token=eb3d6b6e-45a0-41bb-a719-9c22c931f83e)

&#x20; Kibana의 Monitoring 화면에서 노드의 역할들을 확인할 수 있습니다. 대부분의 Elastic Stack 모니터링 도구에서 마스터 노드는 별(⭑) 표시로 구분합니다.

![Kibana 모니터링 화면에서 마스터 노드와 데이터 노드 확인](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUyizASxjZepqPV1G1%2F-LnKqZrQzuaRIRdn6sLK%2Fimage.png?alt=media\&token=46d874f2-f582-4339-8d0f-aef794a3ae93)

{% hint style="danger" %}
실제 운영 환경에서는 위 예제처럼 마스터 후보를 노드는 1개만 설정하면 안 되고 최소 3개 이상의 홀수개로 설정해야 합니다. 이유는 다음의 Split Brain 문제에서 설명합니다.
{% endhint %}

### Split Brain

&#x20; 마스터 후보 노드를 하나만 놓게 되면 그 마스터 노드가 유실되었을 때 클러스터 전체가 작동을 정지 할 위험이 있습니다. 그래서 최소한의 백업용 마스터 노드를 설정하게 되는데 이 때 마스터 후보 노드들은 3개 이상의 홀수 개를 놓는 것을 권장하고 있습니다. 만약에 마스터 후보 노드를 2개 혹은 짝수로 운영하는 경우 네트워크 유실로 인해 다음과 같은 상황을 겪을 수 있습니다.

![네트워크 단절로 인한 클러스터 분리](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUyizASxjZepqPV1G1%2F-LnKtagPohTrQzGWTr4D%2Fimage.png?alt=media\&token=64d5ddfc-0513-48c6-9be4-f3d554df1055)

&#x20; 위와 같이 네트워크 단절로 마스터 후보 노드인 node-1 과 node-2 가 분리되면 각자가 서로 다른 클러스터로 구성되어 계속 동작하는 경우가 있을 수 있습니다. 이 상태에서 각자의 클러스터에 데이터가 추가되거나 변경되고 나면 나중에 네트워크가 복구 되고 하나의 클러스터로 다시 합쳐졌을 때 데이터 정합성에 문제가 생기고 데이터 무결성이 유지될 수 없게 됩니다. 이런 문제를 **Split Brain** 이라고 합니다.

&#x20; Split Brain의 방지를 위해서는 마스터 후보 노드를 3개로 두고 클러스터에 마스터 후보 노드가 최소 2개 이상 존재하고 있을 때에만 클러스터가 동작하고 그렇지 않은 경우 클러스터는 동작을 멈추도록 해야 합니다. **6.x 이전 버전** 에서는 `elasticsearch.yml` 에서 `discovery.zen.minimum_master_nodes` 설정을 이용하여 지정이 가능합니다.

{% code title="elasticsearch.yml" %}

```yaml
discovery.zen.minimum_master_nodes: 2
```

{% endcode %}

&#x20; minimum\_master\_nodes 값은 `(전체 마스터 후보 노드 / 2) + 1` 개로 설정되어야 합니다. 마스터 후보 노드가 5개인 경우 3 으로 설정합니다.

&#x20; 7.0 부터는 `discovery.zen.minimum_master_nodes` 설정이 사라지고 대신 `node.master: true` 인 노드가 추가되면 클러스터가 스스로 minimum\_master\_nodes 노드 값을 변경하도록 되었습니다. 사용자는 최초 마스터 후보로 선출할 `cluster.initial_master_nodes: [ ]` 값만 설정하면 됩니다.

&#x20; 위 설정을 하고 나면 네트워크가 단절 되었을 때 minimum\_master\_nodes 가 2 이상인 클러스터만 살아있고 그렇지 않은 클러스터는 동작을 멈추게 됩니다.

![클러스터 분리 시 마스터 노드가 절반 이상인 클러스터만 생존](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUyizASxjZepqPV1G1%2F-LnKxRWUMYG5ATpsysHP%2Fimage.png?alt=media\&token=560fe149-3215-44d8-8f3c-c6383146c205)

&#x20; 위의 경우 데이터는 노드 node-4, node-5의 데이터만 사용이 가능하고 node-6은 네트워크가 복구될 때 까지 동작하지 않고 노드가 분리되기 이전 상태 그대로 유지됩니다. 이렇게 하면 나중에 클러스터가 복구 되었을 때 node-4, node-5에 추가되거나 변경, 삭제 된 데이터의 정보들만 node-6으로 업데이트 되고 데이터 정합성에는 문제가 없게 됩니다. 이처럼 Split Brain 문제를 피하기 위해서 마스터 후보 노드 개수는 항상 **홀수**로 하고 가동을 위한 최소 마스터 후보 노드 설정은 (전체 마스터 후보 노드)/2+1 로 설정해야 합니다.


# 4. Elasticsearch 데이터 처리

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Elasticsearch는 데이터 저장 형식으로 **json** 도큐먼트를 사용합니다. 데이터 저장 형식 뿐 아니라 쿼리와 클러스터 설정 등 모든 정보를 json 형태로 주고받기 때문에 elasticsearch의 사용을 위해서는 json 사용에 익숙해져야 합니다.


# 4.1 REST API

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Elasticsearch는 http 프로토콜로 접근이 가능한 REST API를 지원합니다. 자원별로 고유 URL로 접근이 가능하며 http 메서드 PUT, POST, GET, DELETE 를 이용해서 자원을 처리합니다. 이런 특성을 가진 시스템을 보통 **RESTFul** 한 시스템이라고 말합니다.

&#x20; REST API가 익숙치 않은 분들을 위해 간단한 비교 설명을 드리면 다음과 같습니다. 사용자 정보를 다루는 **user.com** 이라는 시스템이 있다고 가정하고 **name=kim, age=38, gender=m** 이라는 사용자 정보를 처리한다고 해 보겠습니다. REST를 지원하지 않는 시스템에서는 보통 다음과 같이 각 가능에 대한 개별 페이지로 접근하거나 명령을 매개변수로 처리합니다.

#### RESTFul 하지 않은 시스템에서의 데이터 처리

* 입력 : `http://user.com/input.jsp?name=kim&age=38&gender=m`
* 조회 : `http://user.com/get.jsp?name=kim`
* 삭제 : `http://user.com/delete.jsp?name=kim`

&#x20; REST API를 지원하는 시스템은 kim 이라는 사용자에 대해 항상 단일 URL로 접근을 하고 **PUT, GET, DELETE** 같은 http 메서드로 데이터를 처리합니다

#### RESTFul 한 시스템에서의 데이터 처리

* 입력 : `PUT http://user.com/kim -d {"name":"kim", "age":38, "gender":"m"}`
* 조회 : `GET http://user.com/kim`
* 삭제 : `DELETE http://user.com/kim`

### 유닉스 curl

&#x20; MacOS, 리눅스와 같은 유닉스 기반 운영체제에서는 `curl` 명령어로 간편하게 REST API 사용이 가능합니다. Elasticsearch를 실행한 뒤 curl 명령을 이용해서 elasticsearch 클러스터의 최상위 경로를 호출하며 다음과 같이 클러스터의 상태 정보가 json 형식으로 리턴됩니다.

{% code title="GET 메서드로 elasticsearch 클러스터 조회" %}

```bash
$ curl -XGET "http://localhost:9200"
{
  "name" : "Jongminui-MacBook-Pro.local",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "hpmT8TPiR1Kk69YNao9V3w",
  "version" : {
    "number" : "7.3.0",
    "build_flavor" : "default",
    "build_type" : "tar",
    "build_hash" : "de777fa",
    "build_date" : "2019-07-24T18:30:11.767338Z",
    "build_snapshot" : false,
    "lucene_version" : "8.1.0",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}
```

{% endcode %}

&#x20; 리턴된 결과는 노드명, 클러스터명, Elasticsearch 버전, 루씬 버전 등의 정보들을 담고 있습니다.

### Kibana Dev Tools

&#x20; Rest API를 쉽게 사용하기 위해서는 [포스트맨](https://www.getpostman.com) 같은 도구를 사용할 수 있습니다. Kibana에는 elasticsearch 에서 REST API를 간편하게 실행할 수 있는 **Dev Tools** 라는 도구를 제공합니다.

&#x20; 먼저 Kibana를 실행하기 위해서는 Elastic 홈페이지 (<https://www.elastic.co>)에서 운영체제별로 맞는 Kibana 버전을 내려받아 압축을 풀고 `bin/kibana` 또는 `bin/kibana.bat` (윈도우즈) 를 실행시키면 디폴트로 같은 호스트의 **localhost:9200** 에서 실행중인 elasticsearch와 통신하며 실행이 됩니다.

&#x20; Elasticsearch와 Kibana가 서로 다른 호스트에서 실행되고 있거나 통신 포트가 9200이 아니면 Kibana 홈 config 디렉토리 아래에 있는 `kibana.yml` 파일에서 `elasticsearch.url: "http://localhost:9200"` 옵션을 설정하면 됩니다. 기본적으로 Kibana는 5601 포트에서 실행이 되며 변경하고 싶으면 `server.port: 5601` 을 변경하고 싶은 포트 값으로 설정합니다.

&#x20; Kibana를 실행한 뒤 웹 브라우저를 열고 <http://localhost:5601> 로 접속하면 Kibana를 바로 사용할 수 있습니다. Kibana Dev Tools는 쿼리의 자동 완성도 되고 호스트 경로도 별도로 입력할 필요가 없습니다. 그리고 Dev Tools 에서 입력한 명령을 curl 명령으로 변환하여 클립보드에 복사하는 것도 가능합니다.

![Kibana 의 Dev Tools 메뉴](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnUzV36FUbNFk6kTnaq%2F-LnLrv51XtgjeClkQ9yl%2Fimage.png?alt=media\&token=279ccbcd-03e2-455c-83e2-da88244bfb64)


# 4.2 CRUD - 입력, 조회, 수정, 삭제

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Elasticsearch에서는 단일 도큐먼트별로 고유한 URL을 갖습니다. 도큐먼트에 접근하는 URL은&#x20;

`http://<호스트>:<포트>/<인덱스>/_doc/<도큐먼트 id>`&#x20;

구조로 되어 있습니다. 6.x 이전 까지는

`http://<호스트>:<포트>/<인덱스>/<도큐먼트 타입>/<도큐먼트 id>`

구조였으나, Elasticsearch 7.0 부터는 도큐먼트 타입 개념이 사라지고 대신 고정자 **\_doc** 으로 접근해야 합니다. 다음은 curl 도구를 이용해서 my\_index 인덱스에 도큐먼트 id가 1인 데이터를 입력하는 예제입니다.

{% code title="my\_index/\_doc/1 에 도큐먼트 입력" %}

```bash
$ curl -XPUT "http://localhost:9200/my_index/_doc/1" -H 'Content-Type: application/json' -d'
{
  "name": "Jongmin Kim",
  "message": "안녕하세요 Elasticsearch"
}'
{"_index":"my_index","_type":"_doc","_id":"1","_version":1,"result":"created","_shards":{"total":2,"successful":1,"failed":0},"_seq_no":0,"_primary_term":1}
```

{% endcode %}

&#x20; 이후부터는 elasticsearch의 REST 명령들은 Kibana의 Dev Tools 에서 입력하는 형식으로 설명하도록 하겠습니다. 입력은 `reqest` 탭, 그리고 응답은 `response` 탭에 표기하도록 하겠습니다.

### 입력 (PUT)

&#x20;  데이터 입력을 할 때는 **PUT** 메서드를 이용합니다. 다음은 Kibana 에서 my\_index 인덱스에 도큐먼트 id가 1인 데이터를 입력하는 예제입니다.

{% tabs %}
{% tab title="request" %}
{% code title="my\_index/\_doc/1 최초 입력" %}

```javascript
PUT my_index/_doc/1
{
  "name":"Jongmin Kim",
  "message":"안녕하세요 Elasticsearch"
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="my\_index/\_doc/1 최초 입력 결과" %}

```javascript
{
  "_index" : "my_index",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 처음으로 도큐먼트를 입력하면 결과에 `"result" : "created"` 로 표시가 됩니다. 동일한 URL에 다른 내용의 도큐먼트를 다시 입력하게 되면 기존 도큐먼트는 삭제되고 새로운 도큐먼트로 덮어씌워지게 됩니다. 이 때는 결과에 `created`가 아닌 `updated`가 표시됩니다.

{% tabs %}
{% tab title="request" %}
{% code title="my\_index/\_doc/1 재입력" %}

```javascript
PUT my_index/_doc/1
{
  "name":"Jongmin Kim",
  "message":"안녕하세요 Kibana"
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="my\_index/\_doc/1 재입력 결과" %}

```javascript
{
  "_index" : "my_index",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 2,
  "result" : "updated",
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "failed" : 0
  },
  "_seq_no" : 1,
  "_primary_term" : 1
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 실수로 기존 도큐먼트가 덮어씌워지는 것을 방지하기 위해서는 입력 명령에 \_doc 대신 **\_create** 를 사용해서 새로운 도큐먼트의 입력만 허용하는 것이 가능합니다. 입력하려는 도큐먼트 id에 이미 데이터가 있는 경우 아래와 같이 입력 오류가 나게 됩니다. 6.x 이전 버전에서는 \
`PUT <인덱스>/<도큐먼트 타입>/<도큐먼트 id>/_create`\
형식으로 사용합니다.

{% tabs %}
{% tab title="request" %}
{% code title="\_doc 대신 \_create 로 새 도큐먼트 입력" %}

```javascript
PUT my_index/_create/1
{
  "name":"Jongmin Kim",
  "message":"안녕하세요 Elasticsearch"
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="이미 있는 도큐먼트인 경우 \_create 명령 실행 불가" %}

```javascript
{
  "error": {
    "root_cause": [
      {
        "type": "version_conflict_engine_exception",
        "reason": "[1]: version conflict, document already exists (current version [2])",
        "index_uuid": "qYOJI9ELR2-HqVtgTeI9jw",
        "shard": "0",
        "index": "my_index"
      }
    ],
    "type": "version_conflict_engine_exception",
    "reason": "[1]: version conflict, document already exists (current version [2])",
    "index_uuid": "qYOJI9ELR2-HqVtgTeI9jw",
    "shard": "0",
    "index": "my_index"
  },
  "status": 409
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

### 조회 (GET)

&#x20; GET 메서드로 가져올 도큐먼트의 URL을 입력하면 도큐먼트의 내용을 가져옵니다. 다양한 정보가 함께 표시되며 문서의 내용은 **\_source** 항목에 나타납니다.

{% tabs %}
{% tab title="request" %}
{% code title="my\_index/\_doc/1 도큐먼트 조회" %}

```javascript
GET my_index/_doc/1
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="my\_index/\_doc/1 도큐먼트 조회 결과" %}

```javascript
{
  "_index" : "my_index",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 2,
  "_seq_no" : 1,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "name" : "Jongmin Kim",
    "message" : "안녕하세요 Elasticsearch"
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

### 삭제 (DELETE)

&#x20; DELETE 메서드를 이용해서 도큐먼트 또는 인덱스 단위의 삭제가 가능합니다. 두 경우에 차이가 있는데 먼저 `DELETE my_index/_doc/1` 명령으로 하나의 도큐먼트를 삭제하면 다음과 같이 도큐먼트가 삭제되었다는 `"result" : "deleted"`  결과가 리턴됩니다.

{% tabs %}
{% tab title="request" %}
{% code title="my\_index/\_doc/1 도큐먼트 삭제" %}

```javascript
DELETE my_index/_doc/1
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="my\_index/\_doc/1 도큐먼트 삭제 결과" %}

```javascript
{
  "_index" : "my_index",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 3,
  "result" : "deleted",
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "failed" : 0
  },
  "_seq_no" : 2,
  "_primary_term" : 1
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 도큐먼트는 삭제되었지만 인덱스는 남아있는 경우 삭제된 도큐먼트를 GET 해서 가져오려고 하면 아래와 같이 my\_index/\_doc/1 도큐먼트를 못 찾았다는 `"found" : false` 응답을 받습니다. 인덱스는 있으나 입력되지 않은 조회할 때도 마찬가지 입니다.

{% tabs %}
{% tab title="request" %}
{% code title="삭제된 my\_index/\_doc/1 도큐먼트 조회" %}

```javascript
GET my_index/_doc/1
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="삭제된 my\_index/\_doc/1 도큐먼트 조회 결과" %}

```javascript
{
  "_index" : "my_index",
  "_type" : "_doc",
  "_id" : "1",
  "found" : false
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 이제 `DELETE my_index` 으로 전체 인덱스를 삭제하면 다음과 같이 `"acknowledged" : true` 응답만 리턴됩니다.

{% tabs %}
{% tab title="request" %}
{% code title="my\_index 인덱스 전체 삭제" %}

```javascript
DELETE my_index
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="my\_index 인덱스 전체 삭제 결과" %}

```javascript
{
  "acknowledged" : true
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 삭제된 인덱스 또는 처음부터 없는 인덱스의 도큐먼트를 조회하려고 하면 도큐먼트를 못 찾았다는 `"found" : false` 응답이 아니라 다음과 같이 `"type" : "index_not_found_exception"` , `"status" : 404` 오류가 리턴됩니다.

{% tabs %}
{% tab title="request" %}
{% code title="삭제된 my\_index 인덱스의 도큐먼트 조회" %}

```javascript
GET my_index/_doc/1
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="삭제된 my\_index 인덱스의 조회 결과" %}

```javascript
{
  "error" : {
    "root_cause" : [
      {
        "type" : "index_not_found_exception",
        "reason" : "no such index [my_index]",
        "resource.type" : "index_expression",
        "resource.id" : "my_index",
        "index_uuid" : "_na_",
        "index" : "my_index"
      }
    ],
    "type" : "index_not_found_exception",
    "reason" : "no such index [my_index]",
    "resource.type" : "index_expression",
    "resource.id" : "my_index",
    "index_uuid" : "_na_",
    "index" : "my_index"
  },
  "status" : 404
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

### 수정 (POST)

&#x20; POST 메서드는 PUT 메서드와 유사하게 데이터 입력에 사용이 가능합니다. 도큐먼트를 입력할 때 POST 메서드로 `<인덱스>/_doc` 까지만 입력하게 되면 자동으로 임의의 도큐먼트id 가 생성됩니다. 도큐먼트id의 자동 생성은 PUT 메서드로는 동작하지 않습니다.

{% tabs %}
{% tab title="request" %}
{% code title="POST 명령으로 my\_index/\_doc 도큐먼트 입력" %}

```javascript
POST my_index/_doc
{
  "name":"Jongmin Kim",
  "message":"안녕하세요 Elasticsearch"
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="POST 명령으로 도큐먼트 입력 결과 - 도큐먼트 아이디 자동 생성" %}

```javascript
{
  "_index" : "my_index",
  "_type" : "_doc",
  "_id" : "ZuFv12wBspWtEG13dOut",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

위 결과에서  도큐먼트 id `"_id" : "ZuFv12wBspWtEG13dOut"` 가 자동 생성 된 것을 확인할 수 있습니다.

### \_update

&#x20; 입력된 도큐먼트를 수정하기 위해서는 기존 도큐먼트의 URL에 변경될 내용의 도큐먼트 내용을 다시 PUT 하는 것으로 대치가 가능합니다. 하지만 필드가 여럿 있는 도큐먼트에서 필드 하나만 바꾸기 위해 전체 도큐먼트 내용을 매번 다시 입력하는 것은 번거로운 작업일 것입니다. 이 때는 `POST <인덱스>/_update/<도큐먼트 id>` 명령을 이용해 원하는 필드의 내용만 업데이트가 가능합니다. 업데이트 할 내용에 "doc" 이라는 지정자를 사용합니다.

&#x20; \_update API를 이용해서 `my_index/_doc/1` 도큐먼트의 "message" 필드 값을 *"안녕하세요 Kibana"* 로 업데이트를 한 뒤 도큐먼트 내용을 확인 해 보겠습니다. `my_index/_doc/1` 도큐먼트를 삭제 하였다면 위의 [입력 (PUT)](/04-data/4.2-crud#put) 내용을 참고해서 새로 입력 한 뒤 아래 명령을 실행합니다.

{% tabs %}
{% tab title="request" %}
{% code title="my\_index/\_update/1 도큐먼트의 message 필드 업데이트" %}

```javascript
POST my_index/_update/1
{
  "doc": {
    "message":"안녕하세요 Kibana"
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="my\_index/\_update/1 도큐먼트의 message 필드 업데이트 실행 결과" %}

```javascript
{
  "_index" : "my_index",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 2,
  "result" : "updated",
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "failed" : 0
  },
  "_seq_no" : 1,
  "_primary_term" : 1
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 이제 다시 GET 명령으로 `my_index/_doc/1` 도큐먼트를 조회 해 보면 message 필드가 "안녕하세요 Kibana" 로 변경 된 것을 확인할 수 있습니다.

{% tabs %}
{% tab title="request" %}
{% code title="message 필드 업데이트 후 도큐먼트 확인" %}

```javascript
GET /my_index/_doc/1
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="message 필드 업데이트 후 도큐먼트 확인 결과" %}

```javascript
{
  "_index" : "my_index",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 2,
  "_seq_no" : 1,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "name" : "Jongmin Kim",
    "message" : "안녕하세요 Kibana"
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 위 결과를 보면 `"_version" : 2`로 버전이 증가한 것을 확인할 수 있습니다. \_update API 를 사용해서 단일 필드만 수정하는 경우에도 실제로 내부에서는 도큐먼트 전체 내용을 가져와서 \_doc 에서 지정한 내용을 변경한 새 도큐먼트를 만든 뒤 전체 내용을 다시 PUT 으로 입력하는 작업을 진행합니다.

{% hint style="warning" %}
6.x 이전 버전에서 \_update API 는 \
`POST <인덱스>/<도큐먼트 타입>/<도큐먼트 id>/_update` \
형식으로 사용합니다.
{% endhint %}


# 4.3 벌크 API - \_bulk API

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 여러 명령을 배치로 수행하기 위해서 **\_bulk API**의 사용이 가능합니다. \_bulk API로 **index, create, update, delete**의 동작이 가능하며 delete를 제외하고는 명령문과 데이터문을 한 줄씩 순서대로 입해야 합니다. delete는 내용 입력이 필요 없기 때문에 명령문만 있습니다.

{% hint style="warning" %}
\_bulk 의 명령문과 데이터문은 반드시 한 줄 안에 입력이 되어야 하며 줄바꿈을 허용하지 않습니다.
{% endhint %}

&#x20; 다음은 \_bulk 명령을 실행한 예제입니다. 각 명령의 결과가 items에 배열로 리턴됩니다.

{% tabs %}
{% tab title="request" %}
{% code title="\_bulk 명령 실행" %}

```javascript
POST _bulk
{"index":{"_index":"test", "_id":"1"}}
{"field":"value one"}
{"index":{"_index":"test", "_id":"2"}}
{"field":"value two"}
{"delete":{"_index":"test", "_id":"2"}}
{"create":{"_index":"test", "_id":"3"}}
{"field":"value three"}
{"update":{"_index":"test", "_id":"1"}}
{"doc":{"field":"value two"}}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="\_bulk 명령 실행 결과" %}

```javascript
{
  "took" : 440,
  "errors" : false,
  "items" : [
    {
      "index" : {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "1",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 0,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "index" : {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "2",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 1,
        "_primary_term" : 1,
        "status" : 201
      }
    },
...
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 위 명령이 실행하는 동작들은 다음과 같습니다.

![](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnV0-25VfpscKAFEGLU%2F-LnV09PT6f3bcI4XKuga%2F4.3-01.png?alt=media\&token=2df3f2e8-19a4-48a3-a480-d2bc99e2bdce)

&#x20; 모든 명령이 동일한 인덱스에서 수행되는 경우에는 아래와 같이 `<인덱스명>/_bulk` 형식으로도 사용이 가능합니다.

![인덱스 단위로 \_bulk 사용](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnV0gny70fLgqw3Mckp%2F-LnSJNeZyLy1ANpe6iJp%2Fimage.png?alt=media\&token=c322c735-f2a2-4517-be58-0d3f3a7ab95a)

&#x20; 벌크 동작은 따로따로 수행하는 것 보다 속도가 훨씬 빠릅니다. 특히 대량의 데이터를 입력 할 때는 반드시 \_bulk API를 사용해야 불필요한 오버헤드가 없습니다. **Logstash** 와 **Beats** 그리고 Elastic 웹페이지에서 제공하는 대부분의 언어별 클라이언트에서는 데이터를 입력할 때 \_bulk를 사용하도록 개발되어 있습니다.

{% hint style="danger" %}
Elasticsearch 에는 커밋이나 롤백 등의 트랜잭션 개념이 없습니다. \_bulk 작업 중 연결이 끊어지거나 시스템이 다운되는 등의 이유로 동작이 중단 된 경우에는 어느 동작까지 실행되었는지 확인이 불가능합니다. 보통 이런 경우 전체 인덱스를 삭제하고 처음부터 다시 하는 것이 안전합니다.
{% endhint %}

### 파일에 저장 내용 실행

&#x20; 벌크 명령을 파일로 저장하고 curl 명령으로 실행시킬 수 있습니다. 저장한 명령 파일을 `--data-binary` 로 지정하면 저장된 파일로 부터 입력할 명령과 데이터를 읽어올 수 있습니다. 다음 내용을 **bulk.json** 이라는 이름의 파일로 먼저 저장 해 보겠습니다.

{% code title="bulk.json 파일 내용" %}

```
{"index":{"_index":"test","_id":"1"}}
{"field":"value one"}
{"index":{"_index":"test","_id":"2"}}
{"field":"value two"}
{"delete":{"_index":"test","_id":"2"}}
{"create":{"_index":"test","_id":"3"}}
{"field":"value three"}
{"update":{"_index":"test","_id":"1"}}
{"doc":{"field":"value two"}}
```

{% endcode %}

&#x20;  다음 명령으로 **bulk.json** 파일에 있는 내용들을 \_bulk 명령으로 실행 가능합니다. 파일 이름 앞에는 `@`문자를 입력합니다.

{% code title="bulk.json 파일 내용을 \_bulk로 실행" %}

```bash
$ curl -XPOST "http://localhost:9200/_bulk" -H 'Content-Type: application/json' --data-binary @bulk.json
```

{% endcode %}


# 4.4 검색 API - \_search API

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 지금까지는 도큐먼트 단위의 입력, 수정, 삭제, 조회 하는 방법을 알아보았습니다. 하지만 Elasticsearch의 진가는 쿼리를 통한 검색 기능에 있습니다. 검색은 인덱스 단위로 이루어집니다. `GET <인덱스명>/_search` 형식으로 사용하며 쿼리를 입력하지 않으면 전체 도큐먼트를 찾는 **match\_all** 검색을 합니다.

### URI 검색

&#x20; \_search 뒤에 `q` 파라메터를 사용해서 검색어를 입력할 수 있습니다. 이렇게 요청 주소에 검색어를 넣어 검색하는 방식을 **URI 검색**이라고 합니다.

&#x20; 앞에서 만든 test 인덱스에서 **"value"** 라는 값을 검색하기 위해서는 다음과 같이 입력합니다.

{% tabs %}
{% tab title="request" %}
{% code title="URI 검색으로 검색어 "value" 검색" %}

```javascript
GET test/_search?q=value
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="URI 검색으로 검색어 "value" 검색 결과" %}

```javascript
{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.105360515,
    "hits" : [
      {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.105360515,
        "_source" : {
          "field" : "value three"
        }
      },
      {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.105360515,
        "_source" : {
          "field" : "value two"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 결과를 보면 `hits.total.value` 부분에 검색 결과 전체에 해당되는 문서의 개수가 표시되고 다시 그 안의 `hits:[ ]` 구문 안에 배열로 가장 정확도가 높은 문서 10개가 나타납니다. 이 정확도를 **relevancy**(렐러번시 라고 읽습니다) 라고 하며 뒤에서 다시 설명하도록 하겠습니다.

&#x20; 두 개의 검색어 **"value"** 그리고 **"three"** 를 **AND** 조건으로 검색 하려면 다음과 같이 입력합니다. URI 쿼리에서는 `AND`, `OR`, `NOT` 의 사용이 가능하며 반드시 모두 대문자로 입력해야합니다.

{% tabs %}
{% tab title="request" %}
{% code title="URI 검색으로 검색어 "value AND three" 검색" %}

```javascript
GET test/_search?q=value AND three
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="URI 검색으로 검색어 "value AND three" 검색 결과" %}

```javascript
{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.87546873,
    "hits" : [
      {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.87546873,
        "_source" : {
          "field" : "value three"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; **value** 와 **three** 를 모두 포함한 `test/_doc/3` 도큐먼트 만이 결과로 리턴되었습니다. 검색어 value 을 field 필드에서 찾고 싶으면 다음과 같이 `<필드명>:<검색어>` 형태로 입력합니다. 검색은 항상 필드를 지정해서 하는 것이 좋습니다.

{% tabs %}
{% tab title="request" %}
{% code title="URI 검색으로 "field" 필드에서 검색어 "value" 검색" %}

```javascript
GET test/_search?q=field:value
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="URI 검색으로 "field" 필드에서 검색어 "value" 검색 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.18232156,
    "hits" : [
      {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.18232156,
        "_source" : {
          "field" : "value three"
        }
      },
      {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.18232156,
        "_source" : {
          "field" : "value two"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; URI 검색은 루씬의 기본 쿼리 문법을 사용하며 손쉽게 다룰 수 있습니다. 또한 웹 브라우저 주소창 등에서도 사용 가능하기 때문에 빠르게 쓰긴 쉬우나 좀 더 복잡한 검색을 위해서는 다음에 설명하는 데이터 본문(data body) 검색을 이용해야 합니다.

### 데이터 본문 (Data Body) 검색

&#x20; **데이터 본문(data body) 검색**은 검색 쿼리를 데이터 본문으로 입력하는 방식입니다. Elasticsearch의 QueryDSL을 사용하며 쿼리 또한 Json 형식으로 되어 있습니다. 처음 익힐때는 다소 복잡 해 보일 수 있으나 주로 사용하는 쿼리 몇가지들 부터 차근 차근 익혀나가면 크게 어렵지 않게 사용이 가능합니다.

&#x20; 가장 쉽고 많이 사용되는 것은 **match** 쿼리입니다. 여기서는 문법만 살펴보고 다음 검색 장에서 더 많은 쿼리들에 대해 자세히 다뤄보도록 하겠습니다. 데이터 본문 검색으로 field 필드값이 value 인 도큐먼트를 검색하기 위해서는 다음 명령을 실행합니다.

{% tabs %}
{% tab title="request" %}
{% code title="데이터 본문 검색으로 "field" 필드에서 검색어 "value" 검색" %}

```javascript
GET test/_search
{
  "query": {
    "match": {
      "field": "value"
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="데이터 본문 검색으로 "field" 필드에서 검색어 "value" 검색 결과" %}

```javascript
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.105360515,
    "hits" : [
      {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.105360515,
        "_source" : {
          "field" : "value three"
        }
      },
      {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.105360515,
        "_source" : {
          "field" : "value two"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 쿼리 입력은 항상 `query` 지정자로 시작합니다. 그 다음 레벨에서 쿼리의 종류를 지정하는데 위에서는 **match** 쿼리를 지정했습니다. 그 다음은 사용할 쿼리 별로 문법이 상이할 수 있는데 match 쿼리는 `<필드명>:<검색어>` 방식으로 입력합니다.

### 멀티테넌시 (Multitenancy)

&#x20; Elasticsearch는 여러 개의 인덱스를 한꺼번에 묶어서 검색할 수 있는 **멀티테넌시**를 지원합니다.`logs-2018-01`, `logs-2018-02` … 와 같이 날짜별로 저장된 인덱스들이 있다면 이 인덱스들을 모두 **`logs-*/_search`** 명령으로 한꺼번에 검색이 가능합니다. 특히 시간순으로 따라 쌓이는 로그 데이터를 다룰 때는 인덱스를 일단위 등으로 구분하는것이 좋습니다. 나중에 필드 구조가 변경되거나 크기가 커져서 샤드 설정을 변경하거나 할 때 더욱 용이합니다.

&#x20; 여러 인덱스를 검색할때는 쉼표`,` 로 나열하거나 와일드카드 `*` 문자로 묶을 수 있습니다.

{% code title="쉼표로 나열해서 여러 인덱스 검색" %}

```javascript
GET logs-2018-01,2018-02,2018-03/_search
```

{% endcode %}

{% code title="와일드카드 \* 를 이용해서 여러 인덱스 검색" %}

```javascript
GET logs-2018-*/_search
```

{% endcode %}

{% hint style="danger" %}
인덱스명 대신 `_all` 지정자를 사용하여 `GET _all/_search` 와 같이 실행하면 클러스터에 있는 모든 인덱스를 대상으로 검색이 가능합니다. 하지만 `_all`은 시스템 사용을 위한 인덱스 같은 곳의 데이터까지 접근하여 불필요한 작업 부하를 초래하므로 `_all` 은 되도록 사용하지 않도록 합니다.
{% endhint %}


# 5. 검색과 쿼리 -  Query DSL

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 이번 장은 Elasticsearch의 검색 기능에 대한 전반적인 내용을 모두 설명합니다. 내용이 많지만 Elasticsearch의 동작을 이해하는 데에 중요한 부분이기 때문에 차근차근 읽으면서 잘 숙지하시기 바랍니다.

### 검색 (Search)

&#x20; **검색(retrieval)**&#xC774;란 사전적인 의미로

> 1. **책이나 컴퓨터에 들어 있는 자료 중 필요한 자료를 찾아냄**
> 2. **범죄나 사건을 밝히기 위한 단서나 증거를 찾기 위하여 살펴 조사함**

&#x20; 등을 의미합니다. 필자는 데이터 시스템에서의 검색은&#x20;

> **수많은 대상 데이터 중에서 조건에 부합하는 데이터로 범위를 축소하는 행위**

라고 정의를 합니다.

&#x20; 인터넷 쇼핑몰에 상품이 100만개가 있을 때 검색창에 **"무선 이어폰"** 이라고 입력해서 시스템에 있는 전체 100만개의 상품들 중 무선 이어폰과 연관된 상품만 추려내는 과정을 검색이라고 할 수 있습니다. 검색 엔진 설정에 따라 상품명이 정확히 **"무선 이어폰"** 인 것만 보여줄지, **"소니 무선 이어폰"** 처럼 전체 상품명 중에 검색어를 포함하기만 하면 보여줄지, 가격, 출시일 등과 같이 다른 조건들에 대해서는 어떻게 영향을 받도록 할 것인지 등을 결정할 수 있을 것입니다.

&#x20; 상품명이 정확히 **"무선 이어폰"** 인 것만 검색 하도록 조건을 엄격하게 하면 표시되는 결과 수가 적어져서 내가 찾는 상품이 나타나지 않을 수 있을 것입니다. 반대로 상품 설명에 **"무선"** 과 **"이어폰"** 이 하나라도 있는 상품을 모두 검색하도록 하면 **"무선 리모컨"**, **"이어폰 케이스"** 같은 상품까지 검색이 되면서 결과가 너무 많아져서 내가 찾는 상품이 묻혀 버릴 수 있을 것입니다. 품질이 높은 검색 시스템을 구현하기 위해서는 이렇게 많은 부분들을 고민해야 합니다.

&#x20; Elasticsearch 는 사용자가 이런 여러가지 검색 조건들에 대해 목표로 하는 검색 기능을 구현할 수 있도록 다양한 기능들을 제공합니다. Elasticsearch 는 데이터를 실제로 검색에 사용되는 검색어인 **텀(Term)** 으로 분석 과정을 거쳐 저장하기 때문에 검색 시 대소문자, 단수나 복수, 원형 여부와 상관 없이 검색이 가능합니다. 이러한 Elasticsearch의 특징을 **풀 텍스트 검색(Full Text Search)** 이라고 하며 한국어로 **전문 검색** 이라고도 합니다. 텀(Term)에 대해서는 나중에 텍스트 분석 부분에서 자세히 설명하겠습니다.

### Query DSL (Domain Specific Language)

&#x20; Elasticsearch 는 검색을 위한 쿼리 기능을 제공합니다. 이런 데이터 시스템에서 제공하는 쿼리 기능을 Query DSL (Domain Specific Language) 이라고 이야기 하며 Elasticsearch 의 Query DSL 은 모두 **json** 형식으로 입력해야 합니다.

{% hint style="warning" %}
Basic 라이센스 이상에서는 표준 SQL 문으로 사용 가능한 **Elasticsearch SQL** 기능을 지원합니다. 오라클이나 MySQL 에 익숙한 사람들에게는 처음에는 편할 수 있지만 Elasticsearch 의 모든 검색 기능들을 100% 활용하지 못하기 때문에 json 형식의 쿼리가 다소 생소하더라도 차근 차근 시간을 들여 Elasticsearch 의 Query DSL 을 잘 숙지하며 활용하기를 바랍니다.
{% endhint %}


# 5.1 풀 텍스트 쿼리 - Full Text Query

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Elastcisearch 검색에 사용되는 주요 쿼리들을 살펴보도록 하겠습니다. 예제들을 실행하기 위해 **my\_index** 인덱스에 다음의 5개 도큐먼트를 먼저 입력하도록 하겠습니다.

{% code title="my\_index 인덱스에 벌크로 데이터 입력" %}

```javascript
POST my_index/_bulk
{"index":{"_id":1}}
{"message":"The quick brown fox"}
{"index":{"_id":2}}
{"message":"The quick brown fox jumps over the lazy dog"}
{"index":{"_id":3}}
{"message":"The quick brown fox jumps over the quick dog"}
{"index":{"_id":4}}
{"message":"Brown fox brown dog"}
{"index":{"_id":5}}
{"message":"Lazy jumping dog"}
```

{% endcode %}

&#x20; 이후에 나올 예제들을 확인하기 위해 위에 색인된 도큐먼트들의 message 필드 값의 내용들은 다른 에디터나 노트 등에 적어놓고 같이 보는 것이 편리합니다.

### match\_all

&#x20; match\_all 은 별다른 조건 없이 해당 인덱스의 모든 도큐먼트를 검색하는 쿼리입니다. 검색 시 쿼리를 넣지 않으면 elasticsearch는 자동으로 match\_all을 적용해서 해당 인덱스의 모든 도큐먼트를 검색합니다. 다음 두 예제는 결과가 동일합니다.

{% tabs %}
{% tab title="쿼리 없이 실행" %}

```javascript
GET my_index/_search
```

{% endtab %}

{% tab title="match\_all 쿼리로 실행" %}

```javascript
GET my_index/_search
{
  "query":{
    "match_all":{ }
  }
}

```

{% endtab %}
{% endtabs %}

### match

&#x20; match 쿼리는 풀 텍스트 검색에 사용되는 가장 일반적인 쿼리입니다. 다음은 match 쿼리를 이용하여 my\_index 인덱스의 message 필드에 **dog** 가 포함되어 있는 모든 문서를 검색합니다.

{% tabs %}
{% tab title="request" %}
{% code title="match 쿼리로 message 필드에서 dog 검색" %}

```javascript
GET my_index/_search
{
  "query": {
    "match": {
      "message": "dog"
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="match 쿼리로 message 필드에서 dog 검색 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4,
      "relation" : "eq"
    },
    "max_score" : 0.35847884,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "5",
        "_score" : 0.35847884,
        "_source" : {
          "message" : "Lazy jumping dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 0.32951736,
        "_source" : {
          "message" : "Brown fox brown dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.23470737,
        "_source" : {
          "message" : "The quick brown fox jumps over the lazy dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.23470737,
        "_source" : {
          "message" : "The quick brown fox jumps over the quick dog"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; dog가 포함된 총 4개의 도큐먼트가 검색 결과로 나타납니다.

&#x20; match 검색에 여러 개의 검색어를 집어넣게 되면 디폴트로 **OR** 조건으로 검색이 되어 입력된 검색어 별로 하나라도 포함된 모든 문서를 모두 검색합니다. 다음은 검색어로 **quick dog** 를 검색 한 결과입니다.

{% tabs %}
{% tab title="request" %}
{% code title="match 쿼리로 message 필드에서 quick dog 검색" %}

```javascript
GET my_index/_search
{
  "query": {
    "match": {
      "message": "quick dog"
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="match 쿼리로 message 필드에서 quick dog 검색 결과" %}

```javascript
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 5,
      "relation" : "eq"
    },
    "max_score" : 0.8762741,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.8762741,
        "_source" : {
          "message" : "The quick brown fox jumps over the quick dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.6744513,
        "_source" : {
          "message" : "The quick brown fox jumps over the lazy dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.6173784,
        "_source" : {
          "message" : "The quick brown fox"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "5",
        "_score" : 0.35847884,
        "_source" : {
          "message" : "Lazy jumping dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 0.32951736,
        "_source" : {
          "message" : "Brown fox brown dog"
        }
      }
    ]
  }
}

```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; quick과 dog중 어떤 단어라도 포함한 도큐먼트 총 5개가 검색되었습니다.

&#x20; 검색어가 여럿일 때 검색 조건을 **OR** 가 아닌 **AND** 로 바꾸려면 `operator` 옵션을 사용할 수 있습니다. 이 경우 문법이 조금 달라지는데,\
`<필드명>:<검색어>`\
형식으로 하던 것을\
`<필드명>: { "query":<검색어>, "operator": }`\
와 같이 입력해야 합니다. **quick dog** 를 **AND** 조건으로 검색하려면 다음과 같습니다.

{% tabs %}
{% tab title="request" %}
{% code title="match 쿼리 AND 조건으로 quick dog 검색" %}

```javascript
GET my_index/_search
{
  "query": {
    "match": {
      "message": {
        "query": "quick dog",
        "operator": "and"
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="match 쿼리 AND 조건으로 quick dog 검색 결과" %}

```javascript
{
  "took" : 6,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.8762741,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.8762741,
        "_source" : {
          "message" : "The quick brown fox jumps over the quick dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.6744513,
        "_source" : {
          "message" : "The quick brown fox jumps over the lazy dog"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

### match\_phrase

&#x20; match 쿼리에서 quick 과 dog 검색어를 AND 조건으로 검색하는 방법을 알아보았습니다. 그런데 **"quick dog"** 라는 구문을 공백을 포함해 정확히 일치하는 내용을 검색하려면 어떻게 해야 할까요? 바로 **match\_phrase** 쿼리를 사용하면 됩니다. **match\_phrase** 쿼리는 입력된 검색어를 순서까지 고려하여 검색을 수행합니다. 다음은 **lazy dog** 라는 구문을 검색하는 match\_phrase 쿼리입니다.

{% tabs %}
{% tab title="request" %}
{% code title="match\_phrase 쿼리로 "lazy dog" 구문 검색" %}

```javascript
GET my_index/_search
{
  "query": {
    "match_phrase": {
      "message": "lazy dog"
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="match\_phrase 쿼리로 "lazy dog" 구문 검색 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.9489645,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.9489645,
        "_source" : {
          "message" : "The quick brown fox jumps over the lazy dog"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; **"lazy dog"** 라는 정확한 문장이 포함된 도큐먼트 1개만 검색이 되었습니다.

&#x20; match\_phrase 쿼리는 `slop` 이라는 옵션을 이용하여 `slop`에 지정된 값 만큼 단어 사이에 다른 검색어가 끼어드는 것을 허용할 수 있습니다. `slop`을 1로 하고 검색을 하면 다음과 같은 결과가 나옵니다.

{% tabs %}
{% tab title="request" %}
{% code title="match\_phrase 쿼리에 slop:1 로 "lazy dog" 구문 검색" %}

```javascript
GET my_index/_search
{
  "query": {
    "match_phrase": {
      "message": {
        "query": "lazy dog",
        "slop": 1
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="match\_phrase 쿼리에 slop:1 로 "lazy dog" 구문 검색 결과" %}

```javascript
{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0110221,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "5",
        "_score" : 1.0110221,
        "_source" : {
          "message" : "Lazy jumping dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.9489645,
        "_source" : {
          "message" : "The quick brown fox jumps over the lazy dog"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; slop의 크기를 1로 했기 때문에 lazy와dog 사이에 jumping이 있는 **"Lazy jumping dog"** 값도 검색이 됩니다. slop을 2로 한다면 아마도 lazy jumping brow dog 같은 문장도 검색에 포함될 수 있을 것입니다.

&#x20; 이처럼 match\_phrase 쿼리와 slop을 이용하면 정확도를 조절 해 가며 원하는 검색 결과의 범위를 넓힐 수 있습니다. slop을 너무 크게 하면 검색 범위가 넓어져 관련이 없는 결과가 나타날 확률도 높아지기 때문에 1 이상은 사용하지 않는 것을 권장 드립니다.

### query\_string

&#x20; [4.4 검색 API](/04-data/4.4-_search) 장에서 URL의 q 파라메터를 이용해서 검색을 수행하는 것을 설명했습니다. URL검색에 사용하는 루씬의 검색 문법을 본문 검색에 이용하고 싶을 때 query\_string 쿼리를 사용할 수 있습니다.

&#x20; 다음은 message 필드에서 **lazy**와 **jumping**을 모두 포함하거나 또는 **"quick dog"** 구문을 포함하는 도큐먼트를 검색하는 쿼리입니다. match\_phrase 처럼 구문 검색을 할 때는 검색할 구문을 쌍따옴표 `\"` 안에 넣습니다.

{% tabs %}
{% tab title="request" %}
{% code title="query\_string 쿼리 검색" %}

```javascript
GET my_index/_search
{
  "query": {
    "query_string": {
      "default_field": "message",
      "query": "(jumping AND lazy) OR \"quick dog\""
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="query\_string 쿼리 검색 결과" %}

```javascript
{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 2.818369,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "5",
        "_score" : 2.818369,
        "_source" : {
          "message" : "Lazy jumping dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.67445135,
        "_source" : {
          "message" : "The quick brown fox jumps over the quick dog"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; "Lazy jumping dog" 도큐먼트와 "quick dog" 값을 포함하는 도큐먼트 두개가 결과로 리턴된 것을 확인할 수 있습니다.


# 5.2 Bool 복합 쿼리 - Bool Query

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 앞의 query\_string 쿼리는 여러 조건을 조합하기에는 용이한 문법이지만 옵션이 한정되어 있습니다. 본문 검색에서 여러 쿼리를 조합하기 위해서는 상위에 **bool** 쿼리를 사용하고 그 안에 다른 쿼리들을 넣는 식으로 사용이 가능합니다. bool 쿼리는 다음의 4개의 인자를 가지고 있으며 그 인자 안에 다른 쿼리들을 배열로 넣는 방식으로 동작합니다.

* **must** : 쿼리가 참인 도큐먼트들을 검색합니다.&#x20;
* **must\_not** : 쿼리가 거짓인 도큐먼트들을 검색합니다.&#x20;
* **should** : 검색 결과 중 이 쿼리에 해당하는 도큐먼트의 점수를 높입니다.&#x20;
* **filter** : 쿼리가 참인 도큐먼트를 검색하지만 스코어를 계산하지 않습니다. must 보다 검색 속도가 빠르고 캐싱이 가능합니다.

&#x20; 사용 방법은 다음과 같습니다.

{% code title="bool 쿼리 사용 문법" %}

```javascript
GET <인덱스명>/_search
{
  "query": {
    "bool": {
      "must": [
        { <쿼리> }, …
      ],
      "must_not": [
        { <쿼리> }, …
      ],
      "should": [
        { <쿼리> }, …
      ],
      "filter": [
        { <쿼리> }, …
      ]
    }
  }
}
```

{% endcode %}

다음은 단어 "quick"과 구문 "lazy dog"가 포함된 모든 문서를 검색하는 쿼리입니다.

{% tabs %}
{% tab title="request" %}
{% code title="bool 쿼리로 quick 그리고 "lazy dog" 검색" %}

```javascript
GET my_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "message": "quick"
          }
        },
        {
          "match_phrase": {
            "message": "lazy dog"
          }
        }
      ]
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="bool 쿼리로 quick 그리고 "lazy dog" 검색 결과" %}

```javascript
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.3887084,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.3887084,
        "_source" : {
          "message" : "The quick brown fox jumps over the lazy dog"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 다음은 단어 "quick" 그리고 구문 "lazy dog"가 하나도 포함되지 않은 문서를 검색합니다.

{% tabs %}
{% tab title="request" %}
{% code title="bool 쿼리로 quick 그리고 "lazy dog" 가 포함되지 않은 문서 검색" %}

```javascript
GET my_index/_search
{
  "query": {
    "bool": {
      "must_not": [
        {
          "match": {
            "message": "quick"
          }
        },
        {
          "match_phrase": {
            "message": "lazy dog"
          }
        }
      ]
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="bool 쿼리로 quick 그리고 "lazy dog" 가 포함되지 않은 문서 검색 결과" %}

```javascript
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.0,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 0.0,
        "_source" : {
          "message" : "Brown fox brown dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "5",
        "_score" : 0.0,
        "_source" : {
          "message" : "Lazy jumping dog"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 이렇게 bool 쿼리를 이용해서 복합적인 검색 기능을 구현할 수 있습니다. 특히 bool쿼리는 것은 이후에 설명할 정확도(Relevancy)의 예제를 위해서도 필요합니다.

&#x20; bool 쿼리의 **must**, **should** 등은 표준 SQL의 **AND**, **OR** 등과 **유사하지만 정확히 같지는 않습니다.** must는 SQL의 **AND** 연산자와 유사하게 동작하지만 bool 쿼리에는 표준 SQL의 **OR** 와 정확히 일치하게 동작한다고 할 수 있는 연산자는 없어서 처음에는 이해하기가 조금 어렵습니다.

&#x20; 표준 SQL의 AND, OR 조건 들은 2개의 조건값에 대한 **이항 연산자** 입니다. 하지만 Elasticsearch의 must, must\_not, should 등은 내부에 있는 각각의 쿼리들에 대해 이 쿼리는 참 또는 거짓으로 적용하는 **단항 연산자**라고 생각을 하면 조금 더 이해하기 쉽습니다.

![표준 SQL 과 Elasticsearch Bool 쿼리 비교](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnkDY-4MZD1V45rlVio%2F-LnkDaO7ZsRAg85vnPxB%2F5.2-01.png?alt=media\&token=bfe13e1d-01e6-4d01-b36c-9112d3ad4a7d)

&#x20; **`(A or B) and (not C)`** 에 대한 쿼리를 하려면 elasticsearch의 경우 다음처럼 **A**와 **B**의 OR 조건의 match 쿼리로 하여 must 안에 넣고 **C**를 must\_not에 넣으면 됩니다.

![표준 SQL 과 Elasticsearch Bool 쿼리 비교](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnkDY-4MZD1V45rlVio%2F-LnkDo_R96o_gyd7QWJk%2F5.2-02.png?alt=media\&token=69136151-e064-4887-b9ec-90ecd422a0d3)


# 5.3 정확도 - Relevancy

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; RDBMS 같은 시스템에서는 쿼리 조건에 부합하는 지만 판단하여 결과를 가져올 뿐 각 결과들이 얼마나 정확한지에 대한 판단은 보통 불가능합니다. Elasticsearch 와 같은 풀 텍스트 검색엔진은 검색 결과가 입력된 검색 조건과 얼마나 정확하게 일치하는 지를 계산하는 알고리즘을 가지고 있어 이 정확도를 기반으로 사용자가 가장 원하는 결과를 먼저 보여줄 수 있습니다. 이 정확한 정도를 **relevancy** 라고 합니다 (렐러번시 라고 읽습니다). 한국어로 번역하면 연관성 또는 관련성 이라고 번역이 되는데, 이 책에서는 이해를 돕기 위해 **정확도** 라는 표현을 쓰겠습니다. 스터디 또는 강연 등에서 언급 하실 때는 원래 용어 relevancy 로 사용하실 것을 권장 드립니다.

&#x20; 검색을 할 때 사용자는 찾고자 하는 정확한 결과만 보고싶어 합니다. 검색 조건에 포함 되더라도 사용자가 찾으려는 결과와 상관 없는 결과는 보여주지 않는 것이 좋습니다. 구글 또는 네이버 같은 웹 검색엔진들도 검색을 하면 찾은 결과들 중에 어떤 것이 사용자가 입력한 검색어와 가장 연관성이 있는지를 계산하여 정확도가 가장 높은 결과들 부터 보여줍니다.

### 스코어 (score) 점수

&#x20; Elasticsearch의 검색 결과에는 스코어 점수가 표시가 됩니다. 이 점수는 검색된 결과가 얼마나 검색 조건과 일치하는지를 나타내며 점수가 높은 순으로 결과를 보여줍니다. 다음의 match 쿼리 결과를 살펴보겠습니다.

{% tabs %}
{% tab title="request" %}
{% code title="quick dog 를 포함한 도큐먼트 검색" %}

```javascript
GET my_index/_search
{
  "query": {
    "match": {
      "message": "quick dog"
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="quick dog 를 포함한 도큐먼트 검색 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 5,
      "relation" : "eq"
    },
    "max_score" : 0.8762741,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.8762741,
        "_source" : {
          "message" : "The quick brown fox jumps over the quick dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.6744513,
        "_source" : {
          "message" : "The quick brown fox jumps over the lazy dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.6173784,
        "_source" : {
          "message" : "The quick brown fox"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "5",
        "_score" : 0.35847884,
        "_source" : {
          "message" : "Lazy jumping dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 0.32951736,
        "_source" : {
          "message" : "Brown fox brown dog"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 각 검색 결과의 `_score` 항목에 스코어 점수가 표시되고 이 점수가 높은 결과부터 나타납니다. 그리고 상단의 `max_score`에는 전체 결과 중에서 가장 높은 점수가 표시됩니다. Elasticsearch 는 이 점수를 계산하기 위해 **BM25** 라는 알고리즘을 이용합니다. BM은 Best Matching 을 뜻합니다.

&#x20; 다음은 **BM25**의 계산식입니다.

![BM25 계산식 - 출처: https://en.wikipedia.org/wiki/Okapi\_BM25](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-Lno9s6Qmb--bpqRtfMx%2F-LnoBbY9kk0HSvQyTvvO%2F5.3-01.svg?alt=media\&token=22316230-8f6a-47ff-b3b3-5fe5e9b66bfc)

&#x20; 복잡해 보이는 이 계산에는 크게 **TF**, **IDF** 그리고 **Field Length** 총 3가지 요소가 사용됩니다.

### TF (Term Frequency)

&#x20; 구글에서 **"쥬라기 공원"** 이라는 검색어로 검색을 한다고 가정 해 보겠습니다. **"쥬라기 공원"**&#xC774;라는 단어가 **5**번 들어 있는 웹 페이지 보다는 **10**번 들어있는 웹 페이지가 내가 보고싶어 하는 정보가 있는 페이지일 확률이 높을 것입니다. 도큐먼트 내에 검색된 **텀(term)**&#xC774; 더 많을수록 점수가 높아지는 것을 **Term Frequency** 라고 합니다.

&#x20; 앞의 검색에서는 값이 "The **quick** brown fox jumps over the **quick** **dog**" 인 도큐먼트가 텀 **quick**, **dog** 총 세개를 포함하고 있어 가장 점수가 높습니다. 포함하고 있는 텀이 증가할수록 아래 그래프와 같이 TF 값도 증가를 하며, BM25에서는 최대 25까지 증가합니다. 즉 25 이상 부터는 TF 점수의 변화가 없습니다.

![출처 : https://opensourceconnections.com/blog/2015/10/16/bm25-the-next-generation-of-lucene-relevation](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnoG1RY615gm8v0Jxha%2F-LnoG53-nth6Y7BZkpSG%2F5.3-02-tf.png?alt=media\&token=62435b47-79d9-4e73-99df-5940a598ba4b)

### IDF (Inverse Document Frequency)

&#x20; 다시 구글에서 **"쥬라기 공원"** 이라는 검색어로 검색을 했을 때 **"쥬라기"** 또는 **"공원"** 중 어떤 단어든 포함하는 페이지들은 검색 결과에 나타날 수 있을 것입니다. 이 때 전체 검색 결과 중에 **"쥬라기"** 가 포함된 결과는 **10**개 **"공원"**&#xC774; 포함된 결과는 **100**개 라고 가정한다면 흔한 단어인 **"공원"** 보다는 희소한 단어인 **"쥬라기"** 가 검색에 더 중요한 텀일 가능성이 높습니다. 검색한 텀을 포함하고 있는 도큐먼트 개수가 많을수록 그 텀의 자신의 점수가 감소하는 것을 **Inverse Document Frequency** 라고 합니다.

&#x20; 앞의 검색 결과 중 값이 "The **quick** brown fox" 인 도큐먼트와 "Lazy jumping **dog**" 인 도큐먼트는 **quick**과 **dog**가 각각 한 번씩만 들어가지만 전체 인덱스를 놓고 보면 **quick**이 들어간 문서는 **3**개, **dog**이 들어간 문서는 **4**개 가 있어 **quick** 이 들어가 있는 결과가 점수가 높습니다. 전체 인덱스에 포함된 텀이 증가할수록 아래 그래프와 같이 IDF 감소하게 됩니다. (그래서 Inverse 입니다)

![출처 : https://opensourceconnections.com/blog/2015/10/16/bm25-the-next-generation-of-lucene-relevation](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LnoGdUUoWZAUrQnYksL%2F-LnoIEr8c1Ghixk14w02%2F5.3-03-idf.png?alt=media\&token=9b0736fc-1e9a-4474-88a2-90aff77a40ce)

### Field Length

&#x20; 도큐먼트에서 필드 길이가 큰 필드 보다는 짧은 필드에 있는 텀의 비중이 클 것입니다. 블로그 포스트를 검색하는 경우 검색 하려는 단어가 **제목**과 **내용** 필드에 모두 있는 경우 텍스트 길이가 긴 **내용** 필드 보다는 텍스트 길이가 짧은 **제목** 필드에 검색어를 포함하고 있는 블로그 포스트가 더 점수가 높게 나타납니다. 다음 **lazy** 를 검색한 쿼리의 결과를 살펴보겠습니다.

{% tabs %}
{% tab title="request" %}
{% code title="검색어 lazy 를 포함한 도큐먼트 검색" %}

```javascript
GET my_index/_search
{
  "query": {
    "match": {
      "message": "lazy"
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="검색어 lazy 를 포함한 도큐먼트 검색 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0909162,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "5",
        "_score" : 1.0909162,
        "_source" : {
          "message" : "Lazy jumping dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.71425706,
        "_source" : {
          "message" : "The quick brown fox jumps over the lazy dog"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 검색 결과에서 **lazy** 를 포함하고 있는 2개 도큐먼트 들이 나타났지만 "The quick brown fox jumps over the lazy dog" 보다 길이가 짧은 "Lazy jumping dog" 가 점수가 더 높게 나타납니다.


# 5.4 Bool : Should

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; **bool** 쿼리의 **should** 는 검색 점수를 조정하기 위해 사용할 수 있습니다. 먼저 match 쿼리로 **fox** 를 포함하고 있는 도큐먼트를 검색 한 결과입니다.

{% tabs %}
{% tab title="request" %}
{% code title="match 쿼리로 fox 검색" %}

```javascript
GET my_index/_search
{
  "query": {
    "match": {
      "message": "fox"
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="match 쿼리로 fox 검색 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4,
      "relation" : "eq"
    },
    "max_score" : 0.32951736,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.32951736,
        "_source" : {
          "message" : "The quick brown fox"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 0.32951736,
        "_source" : {
          "message" : "Brown fox brown dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.23470737,
        "_source" : {
          "message" : "The quick brown fox jumps over the lazy dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.23470737,
        "_source" : {
          "message" : "The quick brown fox jumps over the quick dog"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 이 결과들 중 lazy 가 포함된 결과에 가중치를 줘서 상위로 올리고 싶으면 다음과 같이 **should** 안에 **lazy** 를 찾는 검색을 추가합니다.

{% tabs %}
{% tab title="request" %}
{% code title="fox 검색 결과 중 lazy 를 포함한 결과에 가중치 부여" %}

```javascript
GET my_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "message": "fox"
          }
        }
      ],
      "should": [
        {
          "match": {
            "message": "lazy"
          }
        }
      ]
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="fox 검색 결과 중 lazy 를 포함한 결과에 가중치 부여 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4,
      "relation" : "eq"
    },
    "max_score" : 0.9489644,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.9489644,
        "_source" : {
          "message" : "The quick brown fox jumps over the lazy dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.32951736,
        "_source" : {
          "message" : "The quick brown fox"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 0.32951736,
        "_source" : {
          "message" : "Brown fox brown dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.23470737,
        "_source" : {
          "message" : "The quick brown fox jumps over the quick dog"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 새로운 검색 결과에서 fox만 포함하고 있던 "The quick brown fox" 는 점수가 `"_score" : 0.32951736` 로 이전 match 쿼리와 동일하지만, lazy를 함께 포함하고 있는 "The quick brown fox jumps over the lazy dog" 는 점수가 `"_score" : 0.9489644`로 가중되어 가장 상위에 나타납니다.

&#x20; **should**는 **match\_phrase** 와 함께 유용하게 사용할 수 있습니다. 쇼핑몰 상품 검색 같은 사례에서는 보통 검색어로 입력된 단어가 하나라도 포함된 결과들은 모두 가져오도록 되어 있을 것입니다. 이 때 검색 결과 중에서 입력한 검색어 전체 문장이 정확히 일치하는 결과를 맨 상위에 위치시키면 다른 결과들을 누락시키지 않으면서 사용자가 정확하게 원하는 수준 높은 품질의 결과를 제공할 수 있을 것입니다.

&#x20; 다음은 **lazy** 또는 **dog** 중 하나라 포함된 도큐먼트를 모두 검색하면서 그 중에 **"lazy dog"** 구문을 정확히 포함하는 결과들을 가장 상위로 가져옵니다. **must** 안에 **match** 쿼리로 **lazy** 또는 **dog**가 포함된 모든 도큐먼트를 검색하고 **should** 안에 **match\_phrase** 쿼리를 써서 스코어 점수를 높입니다.

{% tabs %}
{% tab title="request" %}
{% code title="lazy 또는 dog 를 검색하면서 "lazy dog" 구문을 포함한 결과에 가중치 부여" %}

```javascript
GET my_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "message": {
              "query": "lazy dog"
            }
          }
        }
      ],
      "should": [
        {
          "match_phrase": {
            "message": "lazy dog"
          }
        }
      ]
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="lazy 또는 dog 를 검색하면서 "lazy dog" 구문을 포함한 결과에 가중치 부여 결과" %}

```javascript
{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4,
      "relation" : "eq"
    },
    "max_score" : 1.897929,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.897929,
        "_source" : {
          "message" : "The quick brown fox jumps over the lazy dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "5",
        "_score" : 1.449395,
        "_source" : {
          "message" : "Lazy jumping dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 0.32951736,
        "_source" : {
          "message" : "Brown fox brown dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.23470737,
        "_source" : {
          "message" : "The quick brown fox jumps over the quick dog"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 이렇게 **should**와 **match\_phrase**를 응용하면 쇼핑몰에서 **"스키 장갑"** 같은 단어로 검색했을 때 스키 용품들과 각종 장갑들을 모두 가져오면서 그 중 스키 장갑을 가장 상위에 표시할 수 있습니다. `slop:1`을 이용하면 "스키 보드 장갑", "스키 벙어리 장갑" 같이 스키와 장갑 사이에 다른 값이 들어간 결과에도 가중치를 부여할 수 있습니다.


# 5.5 정확값 쿼리 - Exact Value Query

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 지금까지 살펴본 풀 텍스트 검색은 스코어 점수 기반으로 **정확도(relevancy)**&#xAC00; 높은 결과부터 가져옵니다. Elasticsearch는 정확도를 고려하는 풀 텍스트 외에도 검색 조건의 **참 / 거짓 여부만 판별**해서 결과를 가져오는 것이 가능합니다. 풀 텍스트와 상반되는 이 특성을 **정확값(Exact Value)** 이라고 하는데 말 그대로 값이 정확히 일치 하는지의 여부 만을 따지는 검색입니다. Exact Value 에는 **term**, **range** 와 같은 쿼리들이 이 부분에 속하며, 스코어를 계산하지 않기 때문에 보통 **bool** 쿼리의 **filter** 내부에서 사용하게 됩니다.

### bool : filter

&#x20; bool쿼리의 filter 안에 하위 쿼리를 사용하면 스코어에 영향을 주지 않습니다. 다음 3개의 검색 결과를 비교 해 보도록 하겠습니다.

{% tabs %}
{% tab title="request" %}
{% code title="match 쿼리로 fox 검색" %}

```javascript
GET my_index/_search
{
  "query": {
    "match": {
      "message": "fox"
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="match 쿼리로 fox 검색 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4,
      "relation" : "eq"
    },
    "max_score" : 0.32951736,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.32951736,
        "_source" : {
          "message" : "The quick brown fox"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 0.32951736,
        "_source" : {
          "message" : "Brown fox brown dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.23470737,
        "_source" : {
          "message" : "The quick brown fox jumps over the lazy dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.23470737,
        "_source" : {
          "message" : "The quick brown fox jumps over the quick dog"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="request" %}
{% code title="match 쿼리로 fox 와 quick 검색" %}

```javascript
GET my_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "message": "fox"
          }
        },
        {
          "match": {
            "message": "quick"
          }
        }
      ]
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="match 쿼리로 fox 와 quick 검색 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 0.9468958,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.9468958,
        "_source" : {
          "message" : "The quick brown fox"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.8762741,
        "_source" : {
          "message" : "The quick brown fox jumps over the quick dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.6744513,
        "_source" : {
          "message" : "The quick brown fox jumps over the lazy dog"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="request" %}
{% code title="must 로 fox 및 filter 으로 quick 검색" %}

```javascript
GET my_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "message": "fox"
          }
        }
      ],
      "filter": [
        {
          "match": {
            "message": "quick"
          }
        }
      ]
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="must 로 fox 및 filter 으로 quick 검색 결과" %}

```javascript
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 0.32951736,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.32951736,
        "_source" : {
          "message" : "The quick brown fox"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.23470737,
        "_source" : {
          "message" : "The quick brown fox jumps over the lazy dog"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.23470737,
        "_source" : {
          "message" : "The quick brown fox jumps over the quick dog"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 첫번째는 **match** 쿼리로 **fox** 를 검색했을 때 **4개**의 도큐먼트가 검색되었고 가장 높은 스코어는 `"_score" : 0.32951736` 입니다. 두번째는 검색에 **match** 쿼리로 **quick** 을 추가했을 때 **3개**의 도큐먼트가 검색되었고 가장 높은 스코어는 `"_score" : 0.9468958` 입니다. 세번째는 첫번째의 검색에 **filter** 구문 안에 **quick** 을 추가했는데 3개의 도큐먼트가 검색되었고 가장 높은 스코어는 첫번째 쿼리와 같은 `"_score" : 0.32951736` 입니다.

&#x20; 이렇게 **filter**는 검색에 조건은 추가하지만 스코어에는 영향을 주지 않도록 제어할 때 사용합니다. 보통 쇼핑몰에서 검색어로 정확도가 높은 상품명을 검색하면서 생산 업체를 다시 필터링 하는 등의 용도로 사용이 가능합니다.

&#x20; **filter** 내부에서 **must\_not** 과 같은 다른 **bool** 쿼리를 포함하려면 **filter** 내부에 **bool** 쿼리를 먼저 넣고 그 안에 다시 **must\_not** 을 넣어야 합니다. 다음은 **fox** 를 포함하면서 **dog** 는 포함하지 않는 도큐먼트를 검색하는 쿼리입니다. **dog** 를 제외하는 **must\_not** 쿼리가 **filter** 안에 있기 때문에 스코어는 **fox** 에만 영향을 받습니다.

{% tabs %}
{% tab title="request" %}
{% code title="must 로 fox 검색 후 must\_not 으로 dog 제거" %}

```javascript
GET my_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "message": "fox"
          }
        }
      ],
      "filter": [
        {
          "bool": {
            "must_not": [
              {
                "match": {
                  "message": "dog"
                }
              }
            ]
          }
        }
      ]
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="must 로 fox 검색 후 must\_not 으로 dog 제거 결과" %}

```javascript
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.32951736,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.32951736,
        "_source" : {
          "message" : "The quick brown fox"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 위 검색에서 결과는 하나만 리턴 되었지만 스코어는 `"_score" : 0.32951736`으로 match 쿼리로 fox만 검색했을 때의 결과와 동일합니다.

### keyword

&#x20; 문자열 데이터는 **keyword** 형식으로 저장하여 정확값 검색이 가능합니다. **Keyword** 에 대해서는 뒤에 **매핑**에서 다시 설명 하겠습니다. 아래의 쿼리는 message 필드값이 **"Brown fox brown dog"** 문자열과 공백, 대소문자까지 정확히 일치하는 데이터만을 결과로 리턴합니다.

{% tabs %}
{% tab title="request" %}
{% code title="keyword 필드 검색" %}

```javascript
GET my_index/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "match": {
            "message.keyword": "Brown fox brown dog"
          }
        }
      ]
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="keyword 필드 검색 결과" %}

```javascript
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.0,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 0.0,
        "_source" : {
          "message" : "Brown fox brown dog"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; keyword 타입으로 저장된 필드는 스코어를 계산하지 않고 정확값의 일치 여부만을 따지기 때문에 스코어가 `"_score" : 0.0` 으로 나오게 됩니다. 스코어를 계산하지 않기 때문에 **keyword** 값을 검색 할 때는 **filter** 구문 안에 넣도록 합니다.

{% hint style="info" %}
filter 안에 넣은 검색 조건들은 스코어를 계산하지 않지만 캐싱이 되기 때문에 쿼리가 더 가볍고 빠르게 실행됩니다. keyword 와 뒤에 설명할 range 쿼리와 같이 스코어 계산이 필요하지 않은 쿼리들은 모두 filter 안에 넣어서 실행하는 것이 좋습니다.
{% endhint %}


# 5.6 범위 쿼리 - Range Query

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 지금까지는 문자열 필드들의 검색에 대해 살펴보았습니다. Elasticsearch는 이 외에도 **숫자**나 **날짜** 형식들의 저장이 가능합니다. 숫자, 날짜 형식은 **range** 쿼리를 이용해서 검색을 합니다.

&#x20; **range** 쿼리의 예제를 위해 먼저 아래의 **phones** 인덱스를 추가하겠습니다.

{% code title="bulk 명령으로 phone 인덱스 추가" %}

```javascript
POST phones/_bulk
{"index":{"_id":1}}
{"model":"Samsung GalaxyS 5","price":475,"date":"2014-02-24"}
{"index":{"_id":2}}
{"model":"Samsung GalaxyS 6","price":795,"date":"2015-03-15"}
{"index":{"_id":3}}
{"model":"Samsung GalaxyS 7","price":859,"date":"2016-02-21"}
{"index":{"_id":4}}
{"model":"Samsung GalaxyS 8","price":959,"date":"2017-03-29"}
{"index":{"_id":5}}
{"model":"Samsung GalaxyS 9","price":1059,"date":"2018-02-25"}
```

{% endcode %}

( 🤓 위 예제 데이터는 실제 상품 정보와 아무런 관련이 없습니다 )

range 쿼리는 `range : { <필드명>: { <파라메터>:<값> } }` 으로 입력됩니다. range 쿼리 파라메터는 아래의 4가지가 있습니다.&#x20;

* **gte** (Greater-than or equal to) - 이상 (같거나 큼)
* **gt** (Greater-than) – 초과 (큼)
* **lte** (Less-than or equal to) - 이하 (같거나 작음)
* **lt** (Less-than) - 미만 (작음)

&#x20; 다음은 phone 인덱스에서 **price** 필드 값이 **700 이상**, **900 미만**인 데이터를 검색하는 쿼리입니다.

{% tabs %}
{% tab title="request" %}
{% code title="price 값이 700 이상 900 미만인 데이터 검색" %}

```javascript
GET phones/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 700,
        "lt": 900
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="price 값이 700 이상 900 미만인 데이터 검색 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "phones",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "model" : "Samsung GalaxyS 6",
          "price" : 795,
          "date" : "2015-03-15"
        }
      },
      {
        "_index" : "phones",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "model" : "Samsung GalaxyS 7",
          "price" : 859,
          "date" : "2016-02-21"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; price 값이 700과 900 사이인 `"price" : 795`, `"price" : 859` 두개의 결과가 리턴 되었습니다.

### 날짜 검색

&#x20; 날짜도 숫자와 마찬가지로 **range** 쿼리의 사용이 가능합니다. 기본적으로 Elasticsearch 에서 날짜 값은 2016-01-01 또는 2016-01-01T10:15:30 과 같이 JSON 에서 일반적으로 사용되는 **ISO8601** 형식을 사용합니다. 다음은 date 필드의 날짜가 2016년 1월 1일 이후인 도큐먼트들을 검색하는 쿼리입니다.

{% tabs %}
{% tab title="request" %}
{% code title="date 값이 2016-01-01 이후인 데이터 검색" %}

```javascript
GET phones/_search
{
  "query": {
    "range": {
      "date": {
        "gt": "2016-01-01"
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="date 값이 2016-01-01 이후인 데이터 검색 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "phones",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "model" : "Samsung GalaxyS 7",
          "price" : 859,
          "date" : "2016-02-21"
        }
      },
      {
        "_index" : "phones",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 1.0,
        "_source" : {
          "model" : "Samsung GalaxyS 8",
          "price" : 959,
          "date" : "2017-03-29"
        }
      },
      {
        "_index" : "phones",
        "_type" : "_doc",
        "_id" : "5",
        "_score" : 1.0,
        "_source" : {
          "model" : "Samsung GalaxyS 9",
          "price" : 1059,
          "date" : "2018-02-25"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 쿼리의 날짜 포맷을 다르게 하고 싶으면 `format` 옵션의 사용이 가능합니다. `||` 을 사용해서 여러 값의 입력이 가능합니다. 아래는 date 필드의 값이 **2015년 12월 31일** 부터 **2018년 이전** 사이에 있는 값들을 검색하는 쿼리입니다.

{% tabs %}
{% tab title="request" %}
{% code title="date 값이 2016-01-01 \~ 2018-01-01 사이의 데이터 검색" %}

```javascript
GET phones/_search
{
  "query": {
    "range": {
      "date": {
        "gt": "31/12/2015",
        "lt": "2018",
        "format": "dd/MM/yyyy||yyyy"
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="date 값이 2016-01-01 \~ 2018-01-01 사이의 데이터 검색 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "phones",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "model" : "Samsung GalaxyS 7",
          "price" : 859,
          "date" : "2016-02-21"
        }
      },
      {
        "_index" : "phones",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 1.0,
        "_source" : {
          "model" : "Samsung GalaxyS 8",
          "price" : 959,
          "date" : "2017-03-29"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 날짜를 검색 할 때는 검색하는 현재 시간을 가져오는 예약어 `now`와 `y`(년), `M`(월), `d`(일), `h`(시), `m`(분), `s`(초), `w`(주) 등의 사용이 가능합니다. 다음은 **date**의 값이 **2016년 1월 1일에서 6개월 후**인 날 부터 **오늘보다 365일 전**인 날 사이의 데이터를 가져오는 쿼리입니다. 참고로 필자가 아래 예제를 실행한 날짜는 **2019년 9월 3일** 입니다.

{% tabs %}
{% tab title="request" %}
{% code title="date 값이 2016-01-01 의 6개월 후 부터 오늘 (2019-09-03) 의 365일 이전 사이 값 검색" %}

```javascript
GET phones/_search
{
  "query": {
    "range": {
      "date": {
        "gt": "2016-01-01||+6M",
        "lt": "now-365d"
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="date 값이 2016-01-01 의 6개월 후 부터 오늘 (2019-09-03) 의 365일 이전 사이 값 검색 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "phones",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 1.0,
        "_source" : {
          "model" : "Samsung GalaxyS 8",
          "price" : 959,
          "date" : "2017-03-29"
        }
      },
      {
        "_index" : "phones",
        "_type" : "_doc",
        "_id" : "5",
        "_score" : 1.0,
        "_source" : {
          "model" : "Samsung GalaxyS 9",
          "price" : 1059,
          "date" : "2018-02-25"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; phones 인덱스의 전체 도큐먼트 중 2016년 1월 1일에 6개월을 더한 **2016-06-01**과 검색을 실행한 2019년 9월 3일 보다 1년 전인 **2018-09-03** 사이의 값인 `"date" : "2017-03-29"`, `"date" : "2018-02-25"` 두 개의 결과가 리턴 되었습니다.

&#x20; 지금까지 살펴본 **range** 쿼리의 스코어는 모두 `"_score" : 1.0`로 동일합니다. **range** 쿼리는 기본적으로 정확도를 계산하지 않습니다. 검색하는 조건이 1000이하라고 할 때 1000에 가까울수록 정확도가 높아지고 1000 보다 크게 낮아질수록 정확도가 떨어지는 것은 아닙니다. 오로지 필드의 값이 1000 보다 같거나 작은지 아닌지의 `true` / `false` 여부만을 판단합니다. 예를 들어 구인 시스템에 입력된 구직자의 입사 지원 조건이 나이 24세부터 55세 사이라고 가정했을 때 구직자의 나이가 35세에 가까울수록 가장 점수가 높고 20대이거나 50대 이면 점수가 낮아지거나 하지 않습니다. range 쿼리는 숫자 또는 날짜가 쿼리 조건에 부합하는지 아닌지의 여부만을 계산합니다.

&#x20; 경우에 따라 range 쿼리에 기준점을 주고 기준점과 가깝거나 멀어질 수록 스코어를 적용할 필요가 있다면 [function\_score 쿼리](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/query-dsl-function-score-query.html)를 사용해서 조정이 가능합니다. function\_score 쿼리는 이 책에서는 다루지 않으니 공식 도큐먼트를 참고하시기 바랍니다.

### 정리&#x20;

&#x20; 이번 장에서는 검색의 개념과 Elasticsearch에서 주로 사용되는 **match, match\_phrase, bool, range** 등의 쿼리들에 대해 알아보았습니다. 그리고 검색에 영향을 미치는 **정확도(relevancy)**, 스코어 점수의 개념들에 대해서도 알아보았습니다.

&#x20; Elastic Stack은 검색 외에도 다양한 형태의 데이터 분석 기능들을 제공하지만, 의미 있는 데이터 분석을 위해서는 유효한 데이터의 범위를 확장하고 축소하는 것이 중요하기 때문에 기본 기능인 검색을 잘 이해하는 것 또한 필요합니다. 이번 장에서 살펴본 쿼리 외에도 **geo\_point** 나 **nested** 같은 특수한 데이터들을 검색하는 쿼리들도 있습니다. 이런 쿼리들은 뒤에서 해당 내용들을 설명하면서 같이 다루도록 하겠습니다.

&#x20; 다음 장에서는 텍스트 데이터의 분석과 색인 과정에 대해 배워보도록 하겠습니다.


# 6. 데이터 색인과 텍스트 분석

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 앞 장에서 Elasticsearch의 다양한 검색 방법에 대해 살펴보았습니다. 풀 텍스트 검색을 하기 위해서는 데이터를 검색에 맞게 가공하는 작업을 필요로 하는데 Elasticsearch는 데이터를 저장하는 과정에서 이 작업을 처리합니다. 이번 장에서는 Elasticsearch가 검색을 위해 텍스트 데이터를 어떻게 처리하고 데이터를 색인 할 때 Elasticsearch에서 어떤 과정이 이루어지는지에 대해 살펴보겠습니다.


# 6.1 역 인덱스 - Inverted Index

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 데이터 시스템에 다음과 같은 문서들을 저장한다고 가정 해 보겠습니다.

![](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LntG3a3EAa6IULTKkT9%2F-LntGEeCVTqaRzzzHFem%2F6.1-01.png?alt=media\&token=ed349e28-8215-43b2-b049-857f68a19d47)

&#x20; 일반적으로 오라클이나 MySQL 같은 관계형 DB에서는 위 내용을 보이는 대로 테이블 구조로 저장을 합니다. 만약에 위 테이블에서 Text 에 **fox**가 포함된 행들을 가져온다고 하면 다음과 같이 Text 열을 한 줄씩 찾아 내려가면서 **fox**가 있으면 가져오고 없으면 넘어가는 식으로 데이터를 가져 올 것입니다.

![테이블 데이터에서 한 줄씩 like 검색](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-Lo--uX4jUMQUTUvBgeF%2F-LntIdlIDXEduASJCXRm%2F6.1-02.png?alt=media\&token=158baec3-905d-4f92-8824-cac8d0239756)

&#x20; 전통적인 RDBMS 에서는 위와 같이 **like** 검색을 사용하기 때문에 데이터가 늘어날수록 검색해야 할 대상이 늘어나 시간도 오래 걸리고, row 안의 내용을 모두 읽어야 하기 때문에 기본적으로 속도가 느립니다. Elasticsearch는 데이터를 저장할 때 다음과 같이 **역 인덱스(inverted index)**&#xB77C;는 구조를 만들어 저장합니다.

![역 인덱스(Inverted Index) 구조](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LntL_BGpuFbNXy_sFtK%2F-LntLbibpXHABupWvXtu%2F6.1-03.png?alt=media\&token=d2726f20-a7ea-4219-bcb0-340cbe1d21f1)

&#x20; 이 역 인덱스는 **책의 맨 뒤에 있는** 주요 키워드에 대한 내용이 몇 페이지에 있는지 볼 수 있는 **찾아보기 페이지**에 비유할 수 있습니다. Elasticsearch에서는 추출된 각 키워드를 **텀(term)** 이라고 부릅니다. 이렇게 역 인덱스가 있으면 **fox**를 포함하고 있는 도큐먼트들의 **id**를 바로 얻어올 수 있습니다.

![](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LntS3nPGQlmuCtaIVJt%2F-LntS6M5Y65sfxz435rP%2F6.1-04.png?alt=media\&token=b8738d24-462e-45d4-8c64-4ed78ceaab15)

&#x20; Elasticsearch는 데이터가 늘어나도 찾아가야 할 행이 늘어나는 것이 아니라 역 인덱스가 가리키는 id의 배열값이 추가되는 것 뿐이기 때문에 큰 속도의 저하 없이 빠른 속도로 검색이 가능합니다. 이런 역 인덱스를 데이터가 저장되는 과정에서 만들기 때문에 Elasticsearch는 데이터를 입력할 때 저장이 아닌 **색인**을 한다고 표현합니다.


# 6.2 텍스트 분석 - Text Analysis

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Elasticsearch에 저장되는 도큐먼트는 모든 **문자열(text)** 필드 별로 역 인덱스를 생성합니다. 검색에 사용하는 경우에는 앞에서 설명한 역 인덱스의 예제는 실제로는 보통 아래와 같이 저장됩니다.

![실제로 역 인덱스에 저장된 텀](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LntVKcOQaeoQJjPnbpP%2F-LntVxXxMRFyBg4lJyRx%2F6.2-01.png?alt=media\&token=7926be1d-e99f-4f5c-8bed-f80708f55931)

&#x20; Elasticsearch는 문자열 필드가 저장될 때 데이터에서 검색어 토큰을 저장하기 위해 여러 단계의 처리 과정을 거칩니다. 이 전체 과정을 **텍스트 분석(Text Analysis)** 이라고 하고 이 과정을 처리하는 기능을 **애널라이저(Analyzer)** 라고 합니다. Elasticsearch의 애널라이저는 0\~3개의 **캐릭터 필터(Character Filter)**&#xC640; 1개의 **토크나이저(Tokenizer)**, 그리고 0\~n개의 **토큰 필터(Token Filter)**&#xB85C; 이루어집니다.

![애널라이저 구성 : 캐릭터 필터 - 토크나이저 - 토큰필터](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LntYrdKmTe441TqYAJl%2F-LntZ63SAIfHu6Q_OgzJ%2F6.2-02.png?alt=media\&token=52213afe-e6ab-4bc2-b9e0-20027542a79e)

&#x20; 텍스트 데이터가 입력되면 가장 먼저 필요에 따라 전체 문장에서 특정 문자를 대치하거나 제거하는데 이 과정을 담당하는 기능이 **캐릭터 필터**입니다. 뒤에서 자세히 설명하고 지금 설명 할 예제에서는 적용하지 않겠습니다.

&#x20; 다음으로는 문장에 속한 단어들을 텀 단위로 하나씩 분리 해 내는 처리 과정을 거치는데 이 과정을 담당하는 기능이 **토크나이저** 입니다. 토크나이저는 **반드시 1개**만 적용이 가능합니다. 다음은 `whitespace` 토크나이저를 이용해서 공백을 기준으로 텀 들을 분리 한 결과입니다.

![whitespace 토크나이저 적용](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LntL_BGpuFbNXy_sFtK%2F-LntLbibpXHABupWvXtu%2F6.1-03.png?alt=media\&token=d2726f20-a7ea-4219-bcb0-340cbe1d21f1)

&#x20; 다음으로 분리된 텀 들을 하나씩 가공하는 과정을 거치는데 이 과정을 담당하는 기능이 **토큰 필터** 입니다. 토큰 필터는 0개 부터 여러 개를 적용할 수 있습니다.

&#x20; 여기서는 먼저 `lowercase` 토큰 필터를 이용해서 대문자를 모두 소문자로 바꿔줍니다. 이렇게 하면 대소문자 구별 없이 검색이 가능하게 됩니다. 대소문자가 일치하게 되어 같은 텀이 된 토큰들은 모두 하나로 병합이 됩니다.

![소문자로 변경 후 같은 텀 병합](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LntbFF1Cbw9kue34dxC%2F-LntbHMfIKRZOiCl7KmN%2F6.2-03.png?alt=media\&token=91afddea-ec2e-4989-a751-20a689374b08)

&#x20; 이제 역 인덱스는 아래와 같이 변경됩니다.

![병합이 완료 된 텀](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LntbFF1Cbw9kue34dxC%2F-LntcLPw_rlidqO38odU%2F6.2-04.png?alt=media\&token=52d756b7-9533-492d-999d-0640f775bcd7)

&#x20; 텀 중에는 검색어로서의 가치가 없는 단어들이 있는데 이런 단어를 **불용어(stopword)** 라고 합니다. 보통 **a, an, are, at, be, but, by, do, for, i, no, the, to …** 등의 단어들은 불용어로 간주되어 검색어 토큰에서 제외됩니다. `stop`토큰 필터를 적용하면 우리가 만드는 역 인덱스에서 **the**가 제거됩니다.

![불용어 the 가 제거된 텀](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LntdTZrPbJB3nIxslS_%2F-LntdYna6xmoecLuIbcL%2F6.2-05.png?alt=media\&token=4e537bb0-76a1-4b98-877d-ceabe3e71bd9)

&#x20; 이제 형태소 분석 과정을 거쳐서 문법상 변형된 단어를 일반적으로 검색에 쓰이는 기본 형태로 변환하여 검색이 가능하게 합니다. 영어에서는 형태소 분석을 위해 `snowball` 토큰 필터를 주로 사용하는데 이 필터는 **\~s**, **\~ing** 등을 제거합니다. 그리고 happy, lazy 와 같은 단어들은 happiness, laziness와 같은 형태로도 사용되기 때문에 **\~y** 를 **\~i** 로 변경합니다. `snowball` 토큰 필터를 적용하고 나면 **jumps**와 **jumping**은 모두 **jump**로 변경되고, 동일하게 jump 로 되었기 때문에 하나의 텀으로 병합됩니다.

![snowball 형태소 분석 적용 후 텀 병합](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-Lntet01jJphNCVzIo7v%2F-Lntf24nCf5pgDeswY5d%2F6.2-06.png?alt=media\&token=4140c045-ee24-443f-b927-84cfdad57a9f)

&#x20; 필요에 따라서는 **동의어**를 추가 해 주기도 합니다. `synonym` 토큰 필터를 사용하여 **quick** 텀에 동의어로 **fast**를 지정하면 **fast** 로 검색했을 때도 같은 의미인 **quick** 을 포함하는 도큐먼트가 검색되도록 할 수 있습니다. AWS 와 Amazon 을 동의어로 놓아 amazon을 검색해도 AWS 를 찾을 수 있게 하는 등 실제로도 사용되는 사례가 많습니다.

![quick 과 fast 텀이 동의어로 저장](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LntgOPNccbFlmVJP9gx%2F-LntgR3I2LDe35aKI--u%2F6.2-07.png?alt=media\&token=b758aac1-6f16-4a8f-8649-bd5a131adbbc)


# 6.3 애널라이저 - Analyzer

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Elasticsearch 에는 애널라이저를 조합하고 그 동작을 자세히 확인할 수 있는 API 들이 있습니다. 계속해서 애널라이저와 관련된 기능들에 대해 살펴보겠습니다.


# 6.3.1 \_analyze API

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Elasticsearch 에서는 분석된 문장을 `_analyze` API를 이용해서 확인할 수 있습니다. 토크나이저는 `tokenizer`, 토큰 필터는 `filter` 항목의 값으로 입력하면 됩니다. 토크나이저는 하나만 적용되기 때문에 바로 입력하고, 토큰필터는 여러개를 적용할 수 있기 때문에 **\[ ]** 안에 배열 형식으로 입력합니다. **"The quick brown fox jumps over the lazy dog"** 문장을 `whitespace` 토크나이저와 `lowercase`, `stop`, `snowball` 토큰 필터를 적용하면 다음과 같은 결과를 확인할 수 있습니다.

{% tabs %}
{% tab title="request" %}
{% code title="\_analyzer API 를 이용해서 텍스트 분석" %}

```javascript
GET _analyze
{
  "text": "The quick brown fox jumps over the lazy dog",
  "tokenizer": "whitespace",
  "filter": [
    "lowercase",
    "stop",
    "snowball"
  ]
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="\_analyzer API 를 이용해서 텍스트 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "quick",
      "start_offset" : 4,
      "end_offset" : 9,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "brown",
      "start_offset" : 10,
      "end_offset" : 15,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : "fox",
      "start_offset" : 16,
      "end_offset" : 19,
      "type" : "word",
      "position" : 3
    },
    {
      "token" : "jump",
      "start_offset" : 20,
      "end_offset" : 25,
      "type" : "word",
      "position" : 4
    },
    {
      "token" : "over",
      "start_offset" : 26,
      "end_offset" : 30,
      "type" : "word",
      "position" : 5
    },
    {
      "token" : "lazi",
      "start_offset" : 35,
      "end_offset" : 39,
      "type" : "word",
      "position" : 7
    },
    {
      "token" : "dog",
      "start_offset" : 40,
      "end_offset" : 43,
      "type" : "word",
      "position" : 8
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 토크나이저, 토큰필터를 이용해 처리된 `"token" : "jump"`, `"token" : "lazi"` 같은 결과들을 확인할 수 있습니다.

&#x20; 여러 토큰 필터를 입력 할 때는 순서가 중요하며 만약에 `stop` 토큰 필터를 `lowercase` 보다 먼저 놓게 되면 `stop` 토큰필터 처리시 대문자로 시작하는 **"The"**&#xB294; 불용어로 간주되지 않아 그냥 남아있게 됩니다. 그 후에 `lowercase`가 적용되어 소문자 **"the"**&#xAC00; 최종 검색 텀으로 역 색인에 남아있게 됩니다.

{% tabs %}
{% tab title="request" %}
{% code title="토크나이저 stop 을 lowercase 보다 먼저 처리" %}

```javascript
GET _analyze
{
  "text": "The quick brown fox jumps over the lazy dog",
  "tokenizer": "whitespace",
  "filter": [
    "stop",
    "lowercase",
    "snowball"
  ]
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="토크나이저 stop 을 lowercase 보다 먼저 처리 한 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "the",
      "start_offset" : 0,
      "end_offset" : 3,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "quick",
      "start_offset" : 4,
      "end_offset" : 9,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "brown",
      "start_offset" : 10,
      "end_offset" : 15,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : "fox",
      "start_offset" : 16,
      "end_offset" : 19,
      "type" : "word",
      "position" : 3
    },
    {
      "token" : "jump",
      "start_offset" : 20,
      "end_offset" : 25,
      "type" : "word",
      "position" : 4
    },
    {
      "token" : "over",
      "start_offset" : 26,
      "end_offset" : 30,
      "type" : "word",
      "position" : 5
    },
    {
      "token" : "lazi",
      "start_offset" : 35,
      "end_offset" : 39,
      "type" : "word",
      "position" : 7
    },
    {
      "token" : "dog",
      "start_offset" : 40,
      "end_offset" : 43,
      "type" : "word",
      "position" : 8
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 애널라이저는 `_analyze` API에서 `analyzer` 항목으로 적용해서 사용이 가능합니다. 애널라이저는 **캐릭터 필터**, **토크나이저** 그리고 **토큰 필터**들을 조합해서 사용자 정의 애널라이저를 만들 수도 있고, Elasticsearch 에 사전에 정의되어 있어 바로 사용 가능 한 애널라이저들도 있습니다. 앞서 실행한 `whitespace` 토크나이저 그리고 `lowercase`, `stop`, `snowball` 토큰필터들을 조합한 것 것이 **`snowball`** 애널라이저 입니다. 다음은 **`snowball`** 애널라이저를 적용해서 **"The quick brown fox jumps over the lazy dog"** 문장을 분석한 예제입니다.

{% tabs %}
{% tab title="request" %}
{% code title="snowball 애널라이저로 문장 분석" %}

```javascript
GET _analyze
{
  "text": "The quick brown fox jumps over the lazy dog",
  "analyzer": "snowball"
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="snowball 애널라이저로 문장 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "quick",
      "start_offset" : 4,
      "end_offset" : 9,
      "type" : "<ALPHANUM>",
      "position" : 1
    },
    {
      "token" : "brown",
      "start_offset" : 10,
      "end_offset" : 15,
      "type" : "<ALPHANUM>",
      "position" : 2
    },
    {
      "token" : "fox",
      "start_offset" : 16,
      "end_offset" : 19,
      "type" : "<ALPHANUM>",
      "position" : 3
    },
    {
      "token" : "jump",
      "start_offset" : 20,
      "end_offset" : 25,
      "type" : "<ALPHANUM>",
      "position" : 4
    },
    {
      "token" : "over",
      "start_offset" : 26,
      "end_offset" : 30,
      "type" : "<ALPHANUM>",
      "position" : 5
    },
    {
      "token" : "lazi",
      "start_offset" : 35,
      "end_offset" : 39,
      "type" : "<ALPHANUM>",
      "position" : 7
    },
    {
      "token" : "dog",
      "start_offset" : 40,
      "end_offset" : 43,
      "type" : "<ALPHANUM>",
      "position" : 8
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; **`snowball`** 애널라이저를 사용한 결과는 앞의 **`whitespace`** 토크나이저 그리고 **`lowercase`**, **`stop`**, **`snowball`** 토큰필터를 사용한 결과와 동일하게 나타납니다.

&#x20; 인덱스의 매핑(mappings) 설정에 snowball 애널라이저를 적용하고 "The quick brown fox jumps over the lazy dog" 값을 색인하면 **fox**, **jump**, **lazi** 등의 단어가 검색 텀으로 저장됩니다. `match` 쿼리로 검색을 수행하면 입력한 검색어도 앞에서 적용한 `snowball` 애널라이저를 똑같이 거치게 됩니다. **jumps** 또는 **jumping** 등으로 검색을 수행하면 `lowercase`, `snowball`토큰 필터 등이 적용되어 검색어를 **jump**로 바꾸어 검색합니다.

&#x20; 인덱스에 애널라이저는 아래 예제와 같이 지정합니다. 매핑에 대해서는 다음 장에서 더 자세히 설명하겠습니다.

{% code title="my\_index2 인덱스의 message 필드에 snowball 애널라이저 적용" %}

```javascript
PUT my_index2
{
  "mappings": {
    "properties": {
      "message": {
        "type": "text",
        "analyzer": "snowball"
      }
    }
  }
}
```

{% endcode %}

{% hint style="warning" %}
6.x 이전 버전의 매핑에서는 `"mappings"` |`"properties"`  사이에  도큐먼트 타입 값이 들어갑니다.
{% endhint %}

&#x20; 위에서 생성한 my\_index2 인덱스에 `"message": "The quick brown fox jumps over the lazy dog"` 값을 넣고 `jumping` 으로 검색을 해 보도록 하겠습니다.

{% code title="my\_index2 에 jumps 를 포함하는 도큐먼트 입력" %}

```javascript
PUT my_index2/_doc/1
{
  "message": "The quick brown fox jumps over the lazy dog"
}
```

{% endcode %}

&#x20; match 쿼리로 **jump**, **jumping** 또는 **jumps** 중 어떤 값으로 검색 해도 결과가 나타납니다.

{% tabs %}
{% tab title="request" %}
{% code title="my\_index2 에서 match 쿼리로 jumping 검색" %}

```javascript
GET my_index2/_search
{
  "query": {
    "match": {
      "message": "jumping"
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="my\_index2 에서 match 쿼리로 jumping 검색 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.2876821,
    "hits" : [
      {
        "_index" : "my_index2",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.2876821,
        "_source" : {
          "message" : "The quick brown fox jumps over the lazy dog"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

![jumping 을 검색 할 때 실제로 jump 로 검색됩니다.](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-Lo-RSK5nd1Yqg9JfP5t%2F-Lo-RaIUVVJBiZvec_TD%2F6.3-01.png?alt=media\&token=1bee3e51-1e6d-456e-8af1-66411f2b93df)


# 6.3.2 Term 쿼리

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

Elasticsearch에서 제공하는 쿼리 중에는 `term` 쿼리가 있습니다. `match` 쿼리와 문법은 유사하지만 `term` 쿼리는 입력한 검색어는 애널라이저를 적용하지 않고 입력된 검색어 그대로 일치하는 텀을 찾습니다. 따라서 **jumps**, **jumping** 으로 검색하면 결과가 나타나지 않고 **jump**로 검색해야 결과가 나타납니다.

{% tabs %}
{% tab title="request" %}
{% code title="term 쿼리로 my\_index2 에서 jumps 검색" %}

```javascript
GET my_index2/_search
{
  "query": {
    "term": {
      "message": "jumps"
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="term 쿼리로 my\_index2 에서 jumps 검색 결과" %}

```javascript
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="request" %}
{% code title="term 쿼리로 my\_index2 에서 jump 검색" %}

```javascript
GET my_index2/_search
{
  "query": {
    "term": {
      "message": "jump"
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="term 쿼리로 my\_index2 에서 jump 검색 결과" %}

```javascript
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.2876821,
    "hits" : [
      {
        "_index" : "my_index2",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.2876821,
        "_source" : {
          "message" : "The quick brown fox jumps over the lazy dog"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 이렇게 도큐먼트의 원문은 **jumps** 이지만 어떤 쿼리를 사용하느냐에 따라 원문과 같은 **jumps** 검색어를 넣어도 검색이 되지 않는 경우가 있습니다.

{% hint style="danger" %}
텍스트 분석(Analysis) 과정은 검색에 사용되는 **역 인덱스**에만 관여합니다. 원본 데이터는 변하지 않으므로 쿼리 결과의 **\_source** 항목에는 항상 **원본 데이터**가 나옵니다.
{% endhint %}

&#x20; 지금까지 본 것 처럼 Elasticsearch는 데이터를 실제로 검색에 사용되는 텀(Term) 으로 분석 과정을 거쳐 저장하기 때문에 검색 시 대소문자, 단수나 복수, 원형 여부와 상관 없이 검색이 가능합니다. 이러한 Elasticsearch의 특징을 [**풀 텍스트 검색(Full Text Search)**](/05-search/5.1-query-dsl) 이라고 하며 한국어로 **전문 검색** 이라고도 합니다.

앞에서 설명한 것들 외에도 elasticsearch에서 사용 가능한 애널라이저, 캐릭터 필터, 토크나이저, 토큰필터 들의 목록은 공식 [홈페이지 도큐먼트](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-analyzers.html)에서 확인이 가능합니다.


# 6.3.3 사용자 정의 애널라이저 - Custom Analyzer

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; **\_analyze API**로 애널라이저, 토크나이저, 토큰필터들의 테스트가 가능하지만, 실제로 인덱스에 저장되는 데이터의 처리에 대한 설정은 **애널라이저만 적용할 수 있습니다**. 인덱스 매핑에 애널라이저를 적용 할 때 보통은 이미 정의되어 제공되는 애널라이저 보다는 토크나이저, 토큰필터 등을 조합하여 만든 **사용자 정의 애널라이저(Custom Analyzer)**&#xB97C; 주로 사용합니다. 이미 정의된 애널라이저들은 매핑에 정의한 text 필드의 **analyzer** 항목에 이름을 명시하기만 하면 쉽게 적용이 가능합니다.

&#x20; 이 책에서는 사용자 정의 애널라이저만 설명하겠으니 Elasticsearch에 사전에 만들어진 애널라이저들은 <https://www.elastic.co/> 홈페이지의 공식 도큐먼트를 참고하시기 바랍니다. 매핑에 아무 설정을 하지 않는 경우 디폴트로 적용되는 애널라이저는 **standard** 애널라이저 입니다.

&#x20; 사용자 정의 애널라이저는 인덱스 **settings** 의 `"index" : { "analysis" :` 부분에 정의합니다. 생성한 다음에는 해당 인덱스에서 `GET` 또는 `POST <인덱스명>/_analyze` 명령으로 사용이 가능합니다. 다음은 **my\_index3** 안에 `whitespace` 토큰크나이저 그리고 `lowercase`, `stop`, `snowball` 토큰필터를 사용하는 `my_custom_analyzer` 라는 이름의 애널라이저를 추가하는 예제입니다.

{% code title="my\_index3 인덱스의 settings 안에 my\_custom\_analyzer 생성" %}

```javascript
PUT my_index3
{
  "settings": {
    "index": {
      "analysis": {
        "analyzer": {
          "my_custom_analyzer": {
            "type": "custom",
            "tokenizer": "whitespace",
            "filter": [
              "lowercase",
              "stop",
              "snowball"
            ]
          }
        }
      }
    }
  }
}
```

{% endcode %}

&#x20; 이제 **my\_index3** 에서 **my\_custom\_analyzer**를 사용할 수 있습니다.

{% tabs %}
{% tab title="request" %}
{% code title="\_analyzer API 로 my\_index2 에서 my\_custom\_analyzer 사용" %}

```javascript
GET my_index3/_analyze
{
  "analyzer": "my_custom_analyzer",
  "text": [
    "The quick brown fox jumps over the lazy dog"
  ]
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="\_analyzer API 로 my\_index2 에서 my\_custom\_analyzer 사용 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "quick",
      "start_offset" : 4,
      "end_offset" : 9,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "brown",
      "start_offset" : 10,
      "end_offset" : 15,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : "fox",
      "start_offset" : 16,
      "end_offset" : 19,
      "type" : "word",
      "position" : 3
    },
    {
      "token" : "jump",
      "start_offset" : 20,
      "end_offset" : 25,
      "type" : "word",
      "position" : 4
    },
    {
      "token" : "over",
      "start_offset" : 26,
      "end_offset" : 30,
      "type" : "word",
      "position" : 5
    },
    {
      "token" : "lazi",
      "start_offset" : 35,
      "end_offset" : 39,
      "type" : "word",
      "position" : 7
    },
    {
      "token" : "dog",
      "start_offset" : 40,
      "end_offset" : 43,
      "type" : "word",
      "position" : 8
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

### 사용자 정의 토큰필터

&#x20; 토크나이저, 토큰필터의 경우에도 옵션을 지정하는 경우에는 사용자 정의 토크나이저, 토큰필터로 만들어 추가해야 합니다. 다음은 `stop` 토큰필터에 **"brown"**&#xC744; 불용어로 적용한 **my\_stop\_filter** 사용자 정의 토큰필터를 생성하고 이것을 **my\_custom\_analyzer**에서 사용하도록 설정 한 예제입니다.

{% hint style="warning" %}
아래 명령 실행 전에 기존 my\_index3 인덱스는 먼저 삭제해야 합니다.
{% endhint %}

{% code title="my\_stop\_filter 를 생성 후 my\_custom\_analyzer 에서 사용" %}

```javascript
PUT my_index3
{
  "settings": {
    "index": {
      "analysis": {
        "analyzer": {
          "my_custom_analyzer": {
            "type": "custom",
            "tokenizer": "whitespace",
            "filter": [
              "lowercase",
              "my_stop_filter",
              "snowball"
            ]
          }
        },
        "filter": {
          "my_stop_filter": {
            "type": "stop",
            "stopwords": [
              "brown"
            ]
          }
        }
      }
    }
  }
}
```

{% endcode %}

![filter 에서 선언한 my\_stop\_filter 를 analyzer 에서 사용](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-Lo3VoDvQWW6J1O6mJ7U%2F-Lo3Vs9uXQwN4FydmUDW%2F6.3.3-01.png?alt=media\&token=5819649b-c059-400a-8097-b989c3bb5dab)

&#x20; 이제 다시 **my\_custom\_analyzer**를 사용해서 텍스트를 분석 해 보면 **brown**이 불용어 처리가 되어 사라진 것을 확인할 수 있습니다.

{% tabs %}
{% tab title="request" %}
{% code title="my\_stop\_filter 가 적용된 my\_custom\_analyzer 로 텍스트 분석" %}

```javascript
GET my_index3/_analyze
{
  "analyzer": "my_custom_analyzer",
  "text": [
    "The quick brown fox jumps over the lazy dog"
  ]
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="my\_stop\_filter 가 적용된 my\_custom\_analyzer 로 텍스트 분석 결과 : brown 사라짐" %}

```javascript
{
  "tokens" : [
    {
      "token" : "the",
      "start_offset" : 0,
      "end_offset" : 3,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "quick",
      "start_offset" : 4,
      "end_offset" : 9,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "fox",
      "start_offset" : 16,
      "end_offset" : 19,
      "type" : "word",
      "position" : 3
    },
    {
      "token" : "jump",
      "start_offset" : 20,
      "end_offset" : 25,
      "type" : "word",
      "position" : 4
    },
    {
      "token" : "over",
      "start_offset" : 26,
      "end_offset" : 30,
      "type" : "word",
      "position" : 5
    },
    {
      "token" : "the",
      "start_offset" : 31,
      "end_offset" : 34,
      "type" : "word",
      "position" : 6
    },
    {
      "token" : "lazi",
      "start_offset" : 35,
      "end_offset" : 39,
      "type" : "word",
      "position" : 7
    },
    {
      "token" : "dog",
      "start_offset" : 40,
      "end_offset" : 43,
      "type" : "word",
      "position" : 8
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

### 매핑에 사용자 정의 애널라이저 적용

&#x20; 애널라이저를 실제 인덱스에 입력할 데이터에 적용하려면 **settings** 부분에서 만든 애널라이저를 **mappings** 의 text 필드별로 지정합니다. 앞에서 만든 **my\_custom\_analyzer** 를 **message** 필드에 적용하는 방법은 다음과 같습니다. **setting** 부분은 위 예제와 동일합니다.

{% code title="message 필드에 my\_custom\_analyzer 애널라이저 적용" %}

```javascript
PUT my_index3
{
  "settings": {
    "index": {
      "analysis": {
        "analyzer": {
          "my_custom_analyzer": {
            "type": "custom",
            "tokenizer": "whitespace",
            "filter": [
              "lowercase",
              "my_stop_filter",
              "snowball"
            ]
          }
        },
        "filter": {
          "my_stop_filter": {
            "type": "stop",
            "stopwords": [
              "brown"
            ]
          }
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "message": {
        "type": "text",
        "analyzer": "my_custom_analyzer"
      }
    }
  }
}
```

{% endcode %}

&#x20; 이제 **my\_index** 에 **message** 필드에 입력되는 값은 위에 지정된 **my\_custom\_analyzer** 애널라이저가 적용됩니다. my\_index의 message 필드에 값을 입력하고 검색 해 보면 **brown**은 불용어 처리가 되어 검색되지 않는 것을 확인할 수 있습니다.

{% code title="my\_index3 에 도큐먼트 입력" %}

```javascript
PUT my_index3/_doc/1
{
  "message": "The quick brown fox jumps over the lazy dog"
}
```

{% endcode %}

{% tabs %}
{% tab title="request" %}
{% code title="my\_index3 에서 brown 검색" %}

```javascript
GET my_index3/_search
{
  "query": {
    "match": {
      "message": "brown"
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="my\_index3 에서 brown 검색 결과 : 불용어 처리 되었기 때문에 검색 안됨" %}

```javascript
{
  "took" : 468,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

###


# 6.3.4 텀 벡터 - \_termvectors API

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 색인된 도큐먼트의 역 인덱스의 내용을 확인할 때는 도큐먼트 별로 **\_termvectors** API를이용해서 확인이 가능합니다. `GET <인덱스>/_termvectors/<도큐먼트id>?fields=<필드명>` 형식으로 사용하며 **6.x** 이전 버전에서는 `GET <인덱스>/<도큐먼트 타입>/<도큐먼트id>/_termvectors?fields=<필드명>` 형식으로 사용합니다.&#x20;

&#x20; 다음은 앞에서 입력한 **my\_index3/\_doc/1** 도큐먼트의 **message** 필드를 확인하는 예제입니다.

{% tabs %}
{% tab title="request" %}
{% code title="my\_index3/\_doc/1 도큐먼트의 message 필드의 termvectors 확인" %}

```javascript
GET my_index3/_termvectors/1?fields=message
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="my\_index3/\_doc/1 도큐먼트의 message 필드의 termvectors 확인 결과" %}

```javascript
{
  "_index" : "my_index3",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "took" : 1,
  "term_vectors" : {
    "message" : {
      "field_statistics" : {
        "sum_doc_freq" : 7,
        "doc_count" : 1,
        "sum_ttf" : 8
      },
      "terms" : {
        "dog" : {
          "term_freq" : 1,
          "tokens" : [
            {
              "position" : 8,
              "start_offset" : 40,
              "end_offset" : 43
            }
          ]
        },
        "fox" : {
          "term_freq" : 1,
          "tokens" : [
            {
              "position" : 3,
              "start_offset" : 16,
              "end_offset" : 19
            }
          ]
        },
        "jump" : {
          "term_freq" : 1,
          "tokens" : [
            {
              "position" : 4,
              "start_offset" : 20,
              "end_offset" : 25
            }
          ]
        },
        "lazi" : {
          "term_freq" : 1,
          "tokens" : [
            {
              "position" : 7,
              "start_offset" : 35,
              "end_offset" : 39
            }
          ]
        },
        "over" : {
          "term_freq" : 1,
          "tokens" : [
            {
              "position" : 5,
              "start_offset" : 26,
              "end_offset" : 30
            }
          ]
        },
        "quick" : {
          "term_freq" : 1,
          "tokens" : [
            {
              "position" : 1,
              "start_offset" : 4,
              "end_offset" : 9
            }
          ]
        },
        "the" : {
          "term_freq" : 2,
          "tokens" : [
            {
              "position" : 0,
              "start_offset" : 0,
              "end_offset" : 3
            },
            {
              "position" : 6,
              "start_offset" : 31,
              "end_offset" : 34
            }
          ]
        }
      }
    }
  }
}

```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 여러개의 필드를 같이 확인하고 싶을 때는 `?fields=field1,field2` 처럼 쉼표로 나열해서 볼 수 있습니다.


# 6.4 캐릭터 필터 - Character Filter

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 캐릭터 필터는 텍스트 분석 중 가장 먼저 처리되는 과정으로 색인된 텍스트가 토크나이저에 의해 텀으로 분리되기 전에 전체 문장에 대해 적용되는 일종의 전처리 도구입니다. 이 책에서 설명하는 7.0 버전 기준으로 캐릭터 필터는 **HTML Strip**, **Mapping**, **Pattern Replace** 총 3개가 존재합니다. `char_filter` 항목에 배열로 입력하며 하나만 적용하거나 차례대로 입력해서 3개를 모두 적용할 수도 있습니다.


# 6.4.1 HTML Strip

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 입력된 텍스트가 HTML 인 경우 HTML 태그들을 제거하여 일반 텍스트로 만듭니다. `<>`로 된 태그를 제거할 뿐 아니라 `&nbsp;` 같은 HTML 문법 용어들도 해석합니다. 입력 값은 `html_strip` 입니다.

&#x20; 다음은 HTML Strip 캐릭터 필터를 이용해서 `<p>I&apos;m so <b>happy</b>!</p>` 문장을 처리 한 결과입니다.

{% tabs %}
{% tab title="request" %}
{% code title="char\_filter 를 이용해서 html 문장 처리" %}

```javascript
POST _analyze
{
  "tokenizer": "keyword",
  "char_filter": [
    "html_strip"
  ],
  "text": "<p>I&apos;m so <b>happy</b>!</p>"
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="char\_filter 를 이용해서 html 문장 처리 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : """

I'm so happy!

""",
      "start_offset" : 0,
      "end_offset" : 32,
      "type" : "word",
      "position" : 0
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 모든 태그들이 제거되고 해석되어 **"I'm so happy!"** 라는 문장으로 변경 된 것을 확인할 수 있습니다.

{% hint style="warning" %}
애널라이저는 항상 최소 1개의 토크나이저를 필요로 하기 때문에 캐릭터 필터만 적용하면 오류가 발생합니다. 위 예제에서는 **keyword** 토크나이저를 같이 사용했습니다.
{% endhint %}


# 6.4.2 Mapping

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Mapping 캐릭터 필터를 이용하면 지정한 단어를 다른 단어로 치환이 가능합니다. 특수문자 등을 포함하는 검색 기능을 구현하려는 경우 반드시 적용해야 해서 실제로 캐릭터 필터 중에는 가장 많이 쓰입니다. 다음 예제를 위해 **language** 필드에 값이 **Java**, **C**, **C++** 인 도큐먼트들이 있는 **coding** 인덱스를 생성 해 보겠습니다.

{% code title="coding 인덱스 생성 후 bulk 로 도큐먼트 입력" %}

```javascript
POST coding/_bulk
{"index":{"_id":"1"}}
{"language":"Java"}
{"index":{"_id":"2"}}
{"language":"C"}
{"index":{"_id":"3"}}
{"language":"C++"}
```

{% endcode %}

&#x20; 이제 coding 인덱스에서 match 쿼리로 **C++** 을 검색 해 보겠습니다.

{% tabs %}
{% tab title="request" %}
{% code title="match 쿼리로 C++ 검색" %}

```javascript
GET coding/_search
{
  "query": {
    "match": {
      "language": "C++"
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="match 쿼리로 C++ 검색 결과" %}

```javascript
{
  "took" : 255,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.47000363,
    "hits" : [
      {
        "_index" : "coding",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.47000363,
        "_source" : {
          "language" : "C"
        }
      },
      {
        "_index" : "coding",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.47000363,
        "_source" : {
          "language" : "C++"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; **C++**&#xC744; 검색했는데 값이 **C**, **C++** 인 두개의 도큐먼트가 결과로 나타났습니다. 검색어를 **C** 또는 **c**로 검색을 해 보아도 동일한 결과가 나타납니다. 도큐먼트가 색인 될 때 **standard** 애널라이저가 적용되면서 **C++**&#xC5D0;서 특수문자 **+**&#xB294; 제거 되고 **C**는 소문자로 처리 되면서 실제로 역 인덱는 **c** 가 저장됩니.

![language 필드의 역 인덱스](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-Lo9RC1IGTHiUM5MTQ6N%2F-Lo9RYjx6OQZOx8znsIJ%2F6.4.2-01.png?alt=media\&token=2fd9f15b-90b8-4887-b9f3-76b6f48cb212)

&#x20; Match 쿼리로 검색했을 때 검색어 **C++** 도 마찬가지로 standard 애널라이저가 적용되어 **c** 로 검색을 수행하여 `"_id" : "1"`, `"_id" : "2"` 인 도큐먼트들이 결과로 나타나게 됩니다. standard 뿐 아니라 대다수의 애널라이저들이 특수문자에 대해서는 불용어로 간주하고 제거 해 버리기 때문에 특수문자가 포함된 검색어들을 검색하려면 먼저 특수문자를 다른 문자로 치환해서 저장해야 합니다.

&#x20; **C++** 텀을 **cpp** 처럼 치환하는 방법도 있는데, 이 경우라면 검색될 가능성이 있는 특수문자를 포함하는 모든 텀을 치환 해 주어야 하기 때문에 다소 번거롭습니다. 여기서는 특수문자 `+` 를 `_plus_` 라는 단어로 치환해서 색인을 해 보도록 하겠습니다. coding 인덱스를 삭제하고 `mapping` 캐릭터 필터를 이용해서 인덱스의 매핑을 새로 지정 한 뒤 앞의 \_bulk 명령으로 입력했던 도큐먼트들을 다시 색인 해 보도록 합니다.

{% code title="coding 인덱스에 mapping 캐릭터 필터 설정" %}

```javascript
PUT coding
{
  "settings": {
    "analysis": {
      "analyzer": {
        "coding_analyzer": {
          "char_filter": [
            "cpp_char_filter"
          ],
          "tokenizer": "whitespace",
          "filter": [ "lowercase", "stop", "snowball" ]
        }
      },
      "char_filter": {
        "cpp_char_filter": {
          "type": "mapping",
          "mappings": [ "+ => _plus_", "- => _minus_" ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "language": {
        "type": "text",
        "analyzer": "coding_analyzer"
      }
    }
  }
}
```

{% endcode %}

![사용자 정의 캐릭터 필터, 애널라이저 설정 후 language 필드에 적용](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-Lo9WhHBo1IPdObQBbQx%2F-Lo9WkC1YnAjYUBq5pPI%2F6.4.2-02.png?alt=media\&token=1a50b3d4-8500-45e6-937d-38d289f6a2bf)

&#x20; `+` 기호는 `_plus_` 로, `-` 기호는 `_minus_` 로 치환하는 **cpp\_char\_filter** 라는 캐릭터 필터를 생성했습니다. 그리고 **cpp\_char\_filter** 캐릭터 필터와, whitespace 토크나이저, lowercase, stop, snowball 토큰필터들로 구성된 **coding\_analyzer** 애널라이저를 생성해서 **language** 필드에 적용을 시켰습니다.

&#x20; 이제 **C++** 로 검색을 해 보면 **C++** 를 가지고 있는 도큐먼트 하나만 검색이 됩니다.

{% tabs %}
{% tab title="request" %}
{% code title="match 쿼리로 C++ 검색" %}

```javascript
GET coding/_search
{
  "query": {
    "match": {
      "language": "C++"
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="match 쿼리로 C++ 검색 결과" %}

```javascript
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.9808292,
    "hits" : [
      {
        "_index" : "coding",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.9808292,
        "_source" : {
          "language" : "C++"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20;   새로운 애널라이저 적용 후에 단어 **C++** 는 `+` 가 `_plus_` 로 변경된 **C\_plus\_\_plus\_** 로 치환되어 색인이 됩니다. 이후 토크나이저 토큰필터를 거치며 새로운 역 인덱스 다음과 같이 생성됩니다.

![새로운 설정에서 생성된 language 필드의 역 인덱스](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-Lo9Z-dbZfhwd10bWuip%2F-Lo9Z1gXZvIhluCRnooV%2F6.4.2-03.png?alt=media\&token=7626b8bc-40b1-43bc-91db-47b87060d0f0)

&#x20; match 쿼리로 검색을 하면 검색어 **C++** 역시 동일한 애널라이저가 적용되어 **c\_plus\_\_plus\_** 로 바뀌어 검색됩니다. 그렇기 때문에 텀 **c\_plus\_\_plus\_** 가 해당되는 `"_id" : "3"` 도큐먼트만 검색이 되게 됩니다.


# 6.4.3 Pattern Replace

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Pattern Replace 캐릭터 필터는 **정규식(Regular Expression)**&#xC744; 이용해서 좀더 복잡한 패턴들을 치환할 수 있는 캐릭터 필터입니다. 다음은 **카멜 표기법(camelCase)**&#xC73C;로 된 단어를 대문자가 시작하는 단위 마다 공백을 삽입하여 세부 단어별로 토크나이징 될 수 있도록 **camel** 인덱스에 **camel\_analyzer** 라는 애널라이저를 생성하는 예제입니다.

{% code title="camel 인덱스에 pattern\_replace 캐릭터 필터 설정" %}

```javascript
PUT camel
{
  "settings": {
    "analysis": {
      "analyzer": {
        "camel_analyzer": {
          "char_filter": [
            "camel_filter"
          ],
          "tokenizer": "standard",
          "filter": [
            "lowercase"
          ]
        }
      },
      "char_filter": {
        "camel_filter": {
          "type": "pattern_replace",
          "pattern": "(?<=\\p{Lower})(?=\\p{Upper})",
          "replacement": " "
        }
      }
    }
  }
}
```

{% endcode %}

&#x20; 이제 camel 인덱스에서 **"FooBazBar"** 라는 단어를 분석 해 보면 다음과 같이 **foo**, **baz**, **bar** 세개의 단어로 분리되는 것을 확인할 수 있습니다.

{% tabs %}
{% tab title="request" %}
{% code title="camel\_analyzer 애널라이저로 문장 분석" %}

```javascript
GET camel/_analyze
{
  "analyzer": "camel_analyzer",
  "text": [
    "public void FooBazBar()"
  ]
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="camel\_analyzer 애널라이저로 문장 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "public",
      "start_offset" : 0,
      "end_offset" : 6,
      "type" : "<ALPHANUM>",
      "position" : 0
    },
    {
      "token" : "void",
      "start_offset" : 7,
      "end_offset" : 11,
      "type" : "<ALPHANUM>",
      "position" : 1
    },
    {
      "token" : "foo",
      "start_offset" : 12,
      "end_offset" : 14,
      "type" : "<ALPHANUM>",
      "position" : 2
    },
    {
      "token" : "baz",
      "start_offset" : 15,
      "end_offset" : 17,
      "type" : "<ALPHANUM>",
      "position" : 3
    },
    {
      "token" : "bar",
      "start_offset" : 18,
      "end_offset" : 21,
      "type" : "<ALPHANUM>",
      "position" : 4
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% hint style="warning" %}
캐릭터 필터는 토크나이저가 적용되기 이전에 해당 필드 전체 내용을 치환하는 일종의 전처리 작업입니다. 유사한 기능을 구현하여 적용하더라도 뒤에 나올 토큰필터에서 텀을 처리하는 것과는 결과에 있어 position 같은 값들의 차이가 생깁니다.
{% endhint %}


# 6.5 토크나이저 - Tokenizer

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 데이터 색인 과정에서 검색 기능에 가장 큰 영향을 미치는 단계가 토크나이저 입니다. 데이터 분석 과정에서 토크나이저는 반드시 **한 개**만 사용이 가능하며 `tokenizer` 항목에 단일값으로 설정합니다. 이 책에서는 자주 사용되고 유용한 토크나이저들 위주로 설명하겠습니다.

&#x20; 토크나이저들 중 **NGram**, **Lowercase** 같은 토크나이저들은 대부분은 Standard 토크나이저에 같은 이름의 토큰 필터를 내장한 들입니다. 이 책에서 다루지 않는 토크나이저들은 공식 홈페이지의 도큐먼트를 확인하시기 바랍니다.


# 6.5.1 Standard, Letter, Whitespace

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 일반적으로 가장 많이 사용되고 기능이 유사하지만 분명히 다른 특징이 있는 **Standard**, **Letter**, **Whitespace** 3가지 토크나이저를 먼저 살펴보도록 하겠습니다. 각자 따로 설명하는 것 보다 동일한 문장이 위의 세 토큰 필터에서 어떻게 다르게 분리가 되는지를 살펴보면서 설명을 하겠습니다. 분석할 문장은 **"THE quick.brown\_FOx jumped! @ 3.5 meters."** 입니다.

{% tabs %}
{% tab title="request" %}
{% code title="standard 토크나이저로 문장 분석" %}

```javascript
GET _analyze
{
  "tokenizer": "standard",
  "text": "THE quick.brown_FOx jumped! @ 3.5 meters."
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="standard 토크나이저로 문장 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "THE",
      "start_offset" : 0,
      "end_offset" : 3,
      "type" : "<ALPHANUM>",
      "position" : 0
    },
    {
      "token" : "quick.brown_FOx",
      "start_offset" : 4,
      "end_offset" : 19,
      "type" : "<ALPHANUM>",
      "position" : 1
    },
    {
      "token" : "jumped",
      "start_offset" : 20,
      "end_offset" : 26,
      "type" : "<ALPHANUM>",
      "position" : 2
    },
    {
      "token" : "3.5",
      "start_offset" : 30,
      "end_offset" : 33,
      "type" : "<NUM>",
      "position" : 3
    },
    {
      "token" : "meters",
      "start_offset" : 34,
      "end_offset" : 40,
      "type" : "<ALPHANUM>",
      "position" : 4
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="request" %}
{% code title="letter 토크나이저로 문장 분석" %}

```javascript
GET _analyze
{
  "tokenizer": "letter",
  "text": "THE quick.brown_FOx jumped! @ 3.5 meters."
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="letter 토크나이저로 문장 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "THE",
      "start_offset" : 0,
      "end_offset" : 3,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "quick",
      "start_offset" : 4,
      "end_offset" : 9,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "brown",
      "start_offset" : 10,
      "end_offset" : 15,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : "FOx",
      "start_offset" : 16,
      "end_offset" : 19,
      "type" : "word",
      "position" : 3
    },
    {
      "token" : "jumped",
      "start_offset" : 20,
      "end_offset" : 26,
      "type" : "word",
      "position" : 4
    },
    {
      "token" : "meters",
      "start_offset" : 34,
      "end_offset" : 40,
      "type" : "word",
      "position" : 5
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="request" %}
{% code title="whitespace 토크나이저로 문장 분석" %}

```javascript
GET _analyze
{
  "tokenizer": "whitespace",
  "text": "THE quick.brown_FOx jumped! @ 3.5 meters."
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="whitespace 토크나이저로 문장 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "THE",
      "start_offset" : 0,
      "end_offset" : 3,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "quick.brown_FOx",
      "start_offset" : 4,
      "end_offset" : 19,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "jumped!",
      "start_offset" : 20,
      "end_offset" : 27,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : "@",
      "start_offset" : 28,
      "end_offset" : 29,
      "type" : "word",
      "position" : 3
    },
    {
      "token" : "3.5",
      "start_offset" : 30,
      "end_offset" : 33,
      "type" : "word",
      "position" : 4
    },
    {
      "token" : "meters.",
      "start_offset" : 34,
      "end_offset" : 41,
      "type" : "word",
      "position" : 5
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 앞 예제들의 response 탭을 열어 각각의 결과를 확인 해 보면 다음과 같습니다.

&#x20; 먼저 **Standard** 토크나이저는 공백으로 텀을 구분하면서 "@"과 같은 일부 특수문자를 제거합니다. "jumped!"의 느낌표, "meters."의 마침표 처럼 단어 끝에 있는 특수문자는 제거되지만 "quick.brown\_FOx" 또는 "3.5" 처럼 중간에 있는 마침표나 밑줄 등은 제거되거나 분리되지 않는 것을 확인할 수 있습니다.

&#x20; **Letter** 토크나이저는 알파벳을 제외한 모든 공백, 숫자, 기호들을 기준으로 텀을 분리합니다. "quick.brown\_FOx" 같은 단어도 "quick", "brown", "FOx" 처럼 모두 분리된 것을 확인할 수 있습니다.

&#x20; **Whitespace** 토크나이저는 스페이스, 탭, 그리고 줄바꿈 같은 공백만을 기준으로 텀을 분리합니다. 특수문자 "@" 그리고 "meters." 의 마지막에 있는 마침표도 사라지지 않고 그대로 남아있는 것을 확인할 수 있습니다.

&#x20; 3개의 토크나이저 중에 **Letter** 토크나이저의 경우 검색 범위가 넓어져서 원하지 않는 결과가 많이 나올 수 있고, 반대로 **Whitespace**의 경우 특수문자를 거르지 않기 때문에 정확하게 검색을 하지 않으면 검색 결과가 나오지 않을 수 있습니다. 따라서 보통은 **Standard** 토크나이저를 많이 사용합니다.


# 6.5.2 UAX URL Email

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 주로 사용되는 Standard 토크나이저도 `@`, `/` 같은 특수문자는 공백과 마찬가지로 제거하고 분리합니다. 그런데 요즘의 블로그 포스트나 신문기사 같은 텍스트 들에는 이메일 주소 또는 웹 URL 경로 등이 삽입되어 있는 경우가 상당히 많습니다. 이 경우 Standard 토크나이저를 사용하면 이메일 주소등이 정상적으로 인식되지 않아 문제가 될 수 있는데, 이를 방지하기 위해 사용 가능한 것이 **UAX URL Email** 토크나이저 입니다.

&#x20; **UAX URL Email** 토크나이저는 이메일 주소와 웹 URL 경로는 분리하지 않고 그대로 하나의 텀으로 저장을 합니다. 다음은 **"email address is <my-name@email.com> and website is <https://www.elastic.co>"** 문장을 각각 Standard 그리고 UAX URL Email 토크나이저로 분리 한 결과입니다.

{% tabs %}
{% tab title="request" %}
{% code title="standard 토크나이저로 문장 분석" %}

```javascript
GET _analyze
{
  "tokenizer": "standard",
  "text": "email address is my-name@email.com and website is https://www.elastic.co"
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="standard 토크나이저로 문장 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "email",
      "start_offset" : 0,
      "end_offset" : 5,
      "type" : "<ALPHANUM>",
      "position" : 0
    },
    {
      "token" : "address",
      "start_offset" : 6,
      "end_offset" : 13,
      "type" : "<ALPHANUM>",
      "position" : 1
    },
    {
      "token" : "is",
      "start_offset" : 14,
      "end_offset" : 16,
      "type" : "<ALPHANUM>",
      "position" : 2
    },
    {
      "token" : "my",
      "start_offset" : 17,
      "end_offset" : 19,
      "type" : "<ALPHANUM>",
      "position" : 3
    },
    {
      "token" : "name",
      "start_offset" : 20,
      "end_offset" : 24,
      "type" : "<ALPHANUM>",
      "position" : 4
    },
    {
      "token" : "email.com",
      "start_offset" : 25,
      "end_offset" : 34,
      "type" : "<ALPHANUM>",
      "position" : 5
    },
    {
      "token" : "and",
      "start_offset" : 35,
      "end_offset" : 38,
      "type" : "<ALPHANUM>",
      "position" : 6
    },
    {
      "token" : "website",
      "start_offset" : 39,
      "end_offset" : 46,
      "type" : "<ALPHANUM>",
      "position" : 7
    },
    {
      "token" : "is",
      "start_offset" : 47,
      "end_offset" : 49,
      "type" : "<ALPHANUM>",
      "position" : 8
    },
    {
      "token" : "https",
      "start_offset" : 50,
      "end_offset" : 55,
      "type" : "<ALPHANUM>",
      "position" : 9
    },
    {
      "token" : "www.elastic.co",
      "start_offset" : 58,
      "end_offset" : 72,
      "type" : "<ALPHANUM>",
      "position" : 10
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="request" %}
{% code title="uax\_url\_email 토크나이저로 문장 분석" %}

```javascript
GET _analyze
{
  "tokenizer": "uax_url_email",
  "text": "email address is my-name@email.com and website is https://www.elastic.co"
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="letter 토크나이저로 문장 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "email",
      "start_offset" : 0,
      "end_offset" : 5,
      "type" : "<ALPHANUM>",
      "position" : 0
    },
    {
      "token" : "address",
      "start_offset" : 6,
      "end_offset" : 13,
      "type" : "<ALPHANUM>",
      "position" : 1
    },
    {
      "token" : "is",
      "start_offset" : 14,
      "end_offset" : 16,
      "type" : "<ALPHANUM>",
      "position" : 2
    },
    {
      "token" : "my-name@email.com",
      "start_offset" : 17,
      "end_offset" : 34,
      "type" : "<EMAIL>",
      "position" : 3
    },
    {
      "token" : "and",
      "start_offset" : 35,
      "end_offset" : 38,
      "type" : "<ALPHANUM>",
      "position" : 4
    },
    {
      "token" : "website",
      "start_offset" : 39,
      "end_offset" : 46,
      "type" : "<ALPHANUM>",
      "position" : 5
    },
    {
      "token" : "is",
      "start_offset" : 47,
      "end_offset" : 49,
      "type" : "<ALPHANUM>",
      "position" : 6
    },
    {
      "token" : "https://www.elastic.co",
      "start_offset" : 50,
      "end_offset" : 72,
      "type" : "<URL>",
      "position" : 7
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20;  `"tokenizer": "uax_url_email"` 로 설정하여 텍스트를 분석하면 이메일 주소와 웹 URL은 그대로 남아있는 것을 확인할 수 있습니다.


# 6.5.3 Pattern

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 앞에서 살펴본 토크나이저들은 다소 차이는 있지만 기본적으로는 공백을 기준으로 하여 텀 들을 분리합니다. 분석 할 데이터가 사람이 읽는 일반적인 문장이 아니라 서버 시스템이나 IoT 장비 등에서 수집된 머신 데이터인 경우 공백이 아닌 쉼표나 세로선 같은 기호가 값 항목의 구분자로 사용되는 경우가 종종 있습니다. 이런 특수한 문자를 구분자로 사용하여 텀을 분리하고 싶은 경우 사용할 수 있는 것이 **Pattern** 토크나이저 입니다.

&#x20; **Pattern** 토크나이저는 분리할 패턴을 기호 또는 Java 정규식 형태로 지정할 수 있습니다. 구분자 지정은 `pattern` 항목에 설정합니다. 다음은 인덱스 **pat\_tokenizer**에 슬래시 `/`를 구분자로 하는 **my\_pat\_tokenizer**라는 사용자 정의 토크나이저를 만들고 **"/usr/share/elasticsearch/bin"** 를 분석하는 예제입니다.

{% code title="pat\_tokenizer 인덱스에 my\_pat\_tokenizer 토크나이저 생성" %}

```javascript
PUT pat_tokenizer
{
  "settings": {
    "analysis": {
      "tokenizer": {
        "my_pat_tokenizer": {
          "type": "pattern",
          "pattern": "/"
        }
      }
    }
  }
}
```

{% endcode %}

{% tabs %}
{% tab title="request" %}
{% code title="my\_pat\_tokenizer 토크나이저로 문장 분석" %}

```javascript
GET pat_tokenizer/_analyze
{
  "tokenizer": "my_pat_tokenizer",
  "text": "/usr/share/elasticsearch/bin"
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="my\_pat\_tokenizer 토크나이저로 문장 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "usr",
      "start_offset" : 1,
      "end_offset" : 4,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "share",
      "start_offset" : 5,
      "end_offset" : 10,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "elasticsearch",
      "start_offset" : 11,
      "end_offset" : 24,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : "bin",
      "start_offset" : 25,
      "end_offset" : 28,
      "type" : "word",
      "position" : 3
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; `"pattern": "/"` 같은 단일 기호 외에도 알파벳 대문자를 기준으로 텀을 분리하도록 하는 `"pattern": "(?<=\\p{Lower})(?=\\p{Upper})"` 와 같은 정규식(Regular Expression) 으로도 설정이 가능합니다.


# 6.5.4 Path Hierarchy

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 디렉토리나 파일 경로 등은 흔하게 저장되는 데이터입니다. 앞의 Pattern 토크나이저에서 **"/usr/share/elasticsearch/bin"** 를 실행했을 때는 디렉토리명 들이 각각 하나의 토큰으로 분리 된 것을 확인했습니다. 이 경우 다른 패스에 있는데 하위 디렉토리 명이 같은 경우 데이터 검색에 혼동이 올 수 있습니다.

&#x20; **Path Hierarchy** 토크나이저를 사용하면 경로 데이터를 계층별로 저장해서 하위 디렉토리에 속한 도큐먼트들을 수준별로 검색하거나 집계하는 것이 가능합니다. 다음은 Path Hierarchy 토크나이저로 **"/usr/share/elasticsearch/bin"** 를 분석하는 예제입니다.

{% tabs %}
{% tab title="request" %}
{% code title="path\_hierarchy 토크나이저로 문장 분석" %}

```javascript
POST _analyze
{
  "tokenizer": "path_hierarchy",
  "text": "/usr/share/elasticsearch/bin"
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="path\_hierarchy 토크나이저로 문장 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "/usr",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "/usr/share",
      "start_offset" : 0,
      "end_offset" : 10,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "/usr/share/elasticsearch",
      "start_offset" : 0,
      "end_offset" : 24,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "/usr/share/elasticsearch/bin",
      "start_offset" : 0,
      "end_offset" : 28,
      "type" : "word",
      "position" : 0
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; `delimiter` 항목값으로 경로 구분자를 지정할 수 있습니다. 디폴트는 `/` 입니다. 그리고 `replacement` 옵션을 이용해서 소스의 구분자를 다른 구분자로 대치해서 저장하는 것도 가능합니다. 그 외의 옵션들은 [공식 도큐먼트](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-pathhierarchy-tokenizer.html)를 참고하시기 바랍니다.

&#x20; 다음은 인덱스 **hir\_tokenizer**에 `-` 구분자를 `/` 구분자로 대치하는 **my\_hir\_tokenizer**라는 사용자 정의 토크나이저를 만들고 **"one-two-three"** 문장을 분석하는 예제입니다.

{% code title="hir\_tokenizer 인덱스에 my\_hir\_tokenizer 토크나이저 생성" %}

```javascript
PUT hir_tokenizer
{
  "settings": {
    "analysis": {
      "tokenizer": {
        "my_hir_tokenizer": {
          "type": "path_hierarchy",
          "delimiter": "-",
          "replacement": "/"
        }
      }
    }
  }
}
```

{% endcode %}

{% tabs %}
{% tab title="request" %}
{% code title="hir\_tokenizer 토크나이저로 문장 분석" %}

```javascript
GET hir_tokenizer/_analyze
{
  "tokenizer": "my_hir_tokenizer",
  "text": [
    "one-two-three"
  ]
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="hir\_tokenizer 토크나이저로 문장 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "one",
      "start_offset" : 0,
      "end_offset" : 3,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "one/two",
      "start_offset" : 0,
      "end_offset" : 7,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "one/two/three",
      "start_offset" : 0,
      "end_offset" : 13,
      "type" : "word",
      "position" : 0
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20;&#x20;


# 6.6 토큰 필터 - Token Filter

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 토크나이저를 이용한 텀 분리 과정 이후에는 분리된 각각의 텀 들을 지정한 규칙에 따라 처리를 해 주는데 이 과정을 담당하는 것이 **토큰 필터**입니다. 토큰 필터는 `filter` ***(token\_filter가 아닙니다!)*** 항목에 배열 값으로 나열해서 지정합니다. 하나만 사용하더라도 배열 값으로 입력해야 하며 나열된 순서대로 처리되기 때문에 순서를 잘 고려해서 입력해야 합니다.

&#x20; 토큰 필터 역시 종류가 상당히 많고 계속 업데이트 되기 때문에 이 책에서는 자주 사용되는 토큰 필터 위주로 살펴보겠습니다. [공식 도큐먼트](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenfilters.html)에서 내가 필요로 하는 토큰 필터가 있는지 종종 들러서 살펴보시기 바랍니다.


# 6.6.1 Lowercase, Uppercase

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 영어나 유럽어 기반의 텍스트는 대소문자가 있어 검색할 때는 대소문자에 상관 없이검색이 가능하도록 처리 해 주어야 합니다. 보통은 텀 들을 모두 소문자로 변경하여 저장하는데 이 역할을 하는 것이 **Lowercase** 토큰 필터입니다. **Lowercase** 토큰 필터는 거의 모든 텍스트 검색 사례에서 사용되는 토큰 필터입니다.

&#x20; **Uppercase** 토큰 필터는 모든 텀을 대문자로 변경하는 것 이며 Lowercase 와 동일하게 설정합니다. 다음은 **"Harry Potter and the Philosopher's Stone"** 문장을 lowercase와 uppercase 로 분석한 예제입니다.

{% tabs %}
{% tab title="request" %}
{% code title="lowercase 토큰 필터로 문장 분석" %}

```javascript
GET _analyze
{
  "filter": [ "lowercase" ],
  "text": [ "Harry Potter and the Philosopher's Stone" ]
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="lowercase 토큰 필터로 문장 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "harry potter and the philosopher's stone",
      "start_offset" : 0,
      "end_offset" : 40,
      "type" : "word",
      "position" : 0
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="request" %}
{% code title="uppercase 토큰 필터로 문장 분석" %}

```javascript
GET _analyze
{
  "filter": [ "uppercase" ],
  "text": [ "Harry Potter and the Philosopher's Stone" ]
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="uppercase 토큰 필터로 문장 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "HARRY POTTER AND THE PHILOSOPHER'S STONE",
      "start_offset" : 0,
      "end_offset" : 40,
      "type" : "word",
      "position" : 0
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20;&#x20;


# 6.6.2 Stop

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 블로그 포스트나 뉴스 기사 같은 글에는 검색에서는 큰 의미가 없는 조사나 전치사 등이 많습니다. 영문에서도 **the**, **is**, **a** 같은 단어들은 대부분 검색어로 쓰이지 않는데 이런 단어를 한국어로는 **불용어**, 영어로는 **stopword**라고 합니다. **Stop** 토큰 필터를 적용하면 불용어에 해당되는 텀 들을 제거합니다.

&#x20; `stopwords` 항목에 불용어로 지정할 단어들을 배열 형태로 나열하거나 `"_english_"`, `"_german_"` 같이 언어를 지정해서 해당 언어팩에 있는 불용어를 지정할 수도 있습니다. 지원되는 언어팩은 [공식 도큐먼트](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-stop-tokenfilter.html)에서 확인할 수 있으며 한, 중, 일어 등은 별도의 형태소 분석기를 사용해야 합니다. 불용어 목록을 별도의 텍스트 파일로 저장하고 저장된 파일 경로를 `stopwords_path` 항목의 값으로 지정하여 사용하는 것도 가능합니다.

&#x20; 다음은 **my\_stop** 인덱스에 **"in"**, **"the"**, **"days"** 를 불용어로 처리하는 **my\_stop\_filter** 라는 이름의 `stop` 토큰필터를 정의하고 `lowercase` 필터와 함께 **"Around the World in Eighty Days"** 문장을 분석 하는 예제입니다.

{% hint style="warning" %}
불용어로 처리할 단어들이 소문자이기 때문에 분석할 때는 반드시 lowercase 토큰필터를 먼저 적용해야 합니다.
{% endhint %}

{% code title="my\_stop 인덱스에 my\_stop\_filter 토큰필터 생성" %}

```javascript
PUT my_stop
{
  "settings": {
    "analysis": {
      "filter": {
        "my_stop_filter": {
          "type": "stop",
          "stopwords": [
            "in",
            "the",
            "days"
          ]
        }
      }
    }
  }
}
```

{% endcode %}

{% tabs %}
{% tab title="request" %}
{% code title="my\_stop\_filter 토큰 필터로 문장 분석" %}

```javascript
GET my_stop/_analyze
{
  "tokenizer": "whitespace",
  "filter": [
    "lowercase",
    "my_stop_filter"
  ],
  "text": [ "Around the World in Eighty Days" ]
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="my\_stop\_filter 토큰 필터로 문장 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "around",
      "start_offset" : 0,
      "end_offset" : 6,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "world",
      "start_offset" : 11,
      "end_offset" : 16,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : "eighty",
      "start_offset" : 20,
      "end_offset" : 26,
      "type" : "word",
      "position" : 4
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 이번에는 불용어 **"in"**, **"the"**, **"eighty"**&#xB97C; **my\_stop\_dic.txt** 파일 안에 저장하고 이 파일을 읽어들여 동일한 문장을 분석 해 보는 예제입니다. 불용어는 모두 줄바꿈으로 입력해야 하며 사전 파일 경로는 **elasticsearch** 의 **config** 디렉토리를 기준으로 상대 경로를 지정해야 하며 텍스트 인코딩은 반드시 **UTF-8** 로 되어 있어야 합니다. **my\_stop\_dic.txt** 파일은 **elasticsearch** 홈 아래의 **config/user\_dic** 디렉토리에 저장되었다고 가정하겠습니다.

{% code title="config/user\_dic 디렉토리 생성 후 my\_stop\_dic.txt 파일 생성" %}

```bash
$ mkdir config/user_dic
$ echo 'in
the
eighty' > config/user_dic/my_stop_dic.txt

```

{% endcode %}

{% code title="stopwords\_path 설정을 가진 my\_stop\_filter 토큰필터 생성" %}

```javascript
PUT my_stop
{
  "settings": {
    "analysis": {
      "filter": {
        "my_stop_filter": {
          "type": "stop",
          "stopwords_path": "user_dic/my_stop_dic.txt"
        }
      }
    }
  }
}
```

{% endcode %}

{% tabs %}
{% tab title="request" %}
{% code title="my\_stop\_filter 토큰 필터로 문장 분석" %}

```javascript
GET my_stop/_analyze
{
  "tokenizer": "whitespace",
  "filter": [
    "lowercase",
    "my_stop_filter"
  ],
  "text": [ "Around the World in Eighty Days" ]
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="my\_stop\_filter 토큰 필터로 문장 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "around",
      "start_offset" : 0,
      "end_offset" : 6,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "world",
      "start_offset" : 11,
      "end_offset" : 16,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : "days",
      "start_offset" : 27,
      "end_offset" : 31,
      "type" : "word",
      "position" : 5
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; **my\_stop\_dic.txt** 파일 내용인 **in**, **the**, **eight** 가 제거된 나머지 텀 들만 결과로 나타난 것을 확인할 수 있습니다.

{% hint style="danger" %}
기존의 사전 파일의 내용이 변경 된 경우 인덱스를 새로 고침을 해 주어야 토큰 필터가 새로 적용됩니다. 이것은 **stop** 외에도 뒤에 설명할 **synonym** 이나 **nori 한글 형태소 분석기** 사전에도 동일하게 적용됩니다. 새로 고침을 하는 방법은

**POST <인덱스명>/\_close**\
**POST <인덱스명>/\_open**

을 차례대로 실행 해 주면 됩니다. 인덱스가 close 된 중에는 색인이나 검색이 불가능 하게 되니 주의해야 합니다.

또한 애널라이저의 사전만 갱신되는 것이기 때문에 이미 색인된 도큐먼트들의 역 색인 내용은 변경되지 않습니다. 인덱스 새로 고침 이후에 색인되는 데이터들과 match 쿼리의 검색 등에만 적용이 됩니다. 기존 도큐먼트의 역 색인을 변경하려면 데이터를 모두 다시 재색인을 해야 합니다.
{% endhint %}


# 6.6.3 Synonym

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 검색 서비스에 따라서 **동의어** 검색을 제공해야 하는 경우가 있습니다. 예를 들면 클라우드 서비스 관련 정보를 검색하는 시스템에서 "AWS" 라는 단어를 검색했을 때 "Amazon" 또는 한글 "아마존" 도 같이 검색을 하도록 하면 관련된 정보를 더 많이 찾을 수 있을 것입니다. 이 때 **Synonym** 토큰 필터를 사용하면 텀의 동의어 저장이 가능합니다.&#x20;

&#x20; 동의어를 설정하는 옵션은 `synonyms` 항목에서 직접 동의어 목록을 입력하는 방법과 동의어 사전 파일을 만들어 `synonyms_path` 로 지정하는 방법이 있습니다. 동의어 사전 명시 규칙에는 다음의 것들이 있습니다.

* `"A, B => C"` : 왼쪽의 A, B 대신 오른쪽의 C 텀을 저장합니다. A, B 로는 C 의 검색이 가능하지만 C 로는 A, B 가 검색되지 않습니다.
* `"A, B"` : A, B 각 텀이 A 와 B 두개의 텀을 모두 저장합니다. A 와 B 모두 서로의 검색어로 검색이 됩니다.

&#x20; 다음은 **my\_synonym** 인덱스에 `"amazon => aws"` 으로 동의어를 지정하는 예제입니다.

{% code title=""amazon => aws" 동의어를 지정하는 my\_synonym 인덱스 생성" %}

```javascript
PUT my_synonym
{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_syn": {
          "tokenizer": "whitespace",
          "filter": [
            "lowercase",
            "syn_aws"
          ]
        }
      },
      "filter": {
        "syn_aws": {
          "type": "synonym",
          "synonyms": [
            "amazon => aws"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "message": {
        "type": "text",
        "analyzer": "my_syn"
      }
    }
  }
}
```

{% endcode %}

&#x20; 이제 여기에 **"Amazon Web Service"**, **"AWS"** 값을 가진 도큐먼트 두 개를 저장하고 각 도큐먼트의 `_termvectors` 를 확인 해 보겠습니다.

{% code title="AWS, Amazon Web Service 도큐먼트 저장" %}

```javascript
PUT my_synonym/_doc/1
{ "message" : "Amazon Web Service" }
PUT my_synonym/_doc/2
{ "message" : "AWS" }
```

{% endcode %}

{% tabs %}
{% tab title="request" %}
{% code title="1 도큐먼트 message 필드의 termvectors 확인" %}

```javascript
GET my_synonym/_termvectors/1?fields=message
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="1 도큐먼트 message 필드의 termvectors 확인 결과" %}

```javascript
{
  "_index" : "my_synonym",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "took" : 9,
  "term_vectors" : {
    "message" : {
      "field_statistics" : {
        "sum_doc_freq" : 4,
        "doc_count" : 2,
        "sum_ttf" : 4
      },
      "terms" : {
        "aws" : {
          "term_freq" : 1,
          "tokens" : [
            {
              "position" : 0,
              "start_offset" : 0,
              "end_offset" : 6
            }
          ]
        },
        "service" : {
          "term_freq" : 1,
          "tokens" : [
            {
              "position" : 2,
              "start_offset" : 11,
              "end_offset" : 18
            }
          ]
        },
        "web" : {
          "term_freq" : 1,
          "tokens" : [
            {
              "position" : 1,
              "start_offset" : 7,
              "end_offset" : 10
            }
          ]
        }
      }
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="request" %}
{% code title="2 도큐먼트 message 필드의 termvectors 확인" %}

```javascript
GET my_synonym/_termvectors/2?fields=message
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="2 도큐먼트 message 필드의 termvectors 확인 결과" %}

```javascript
{
  "_index" : "my_synonym",
  "_type" : "_doc",
  "_id" : "2",
  "_version" : 1,
  "found" : true,
  "took" : 0,
  "term_vectors" : {
    "message" : {
      "field_statistics" : {
        "sum_doc_freq" : 4,
        "doc_count" : 2,
        "sum_ttf" : 4
      },
      "terms" : {
        "aws" : {
          "term_freq" : 1,
          "tokens" : [
            {
              "position" : 0,
              "start_offset" : 0,
              "end_offset" : 3
            }
          ]
        }
      }
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 1 도큐먼트의 **"Amazon Web Service"**&#xAC00; **amazon** 대신 **"aws"**, **"web"**, **"service"** 로 저장된 것을 확인할 수 있습니다. 다음은 각각 **term** 쿼리로 **aws**, **amazon** 을 검색한 결과와 **match** 쿼리로 **amazon** 을 검색한 결과입니다.

{% tabs %}
{% tab title="request" %}
{% code title="term 쿼리로 aws 검색" %}

```javascript
GET my_synonym/_search
{
  "query": {
    "term": {
      "message": "aws"
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="term 쿼리로 aws 검색 결과" %}

```javascript
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.22920427,
    "hits" : [
      {
        "_index" : "my_synonym",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.22920427,
        "_source" : {
          "message" : "AWS"
        }
      },
      {
        "_index" : "my_synonym",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.1513613,
        "_source" : {
          "message" : "Amazon Web Service"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="request" %}
{% code title="term 쿼리로 amazon 검색" %}

```javascript
GET my_synonym/_search
{
  "query": {
    "term": {
      "message": "amazon"
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="term 쿼리로 amazon 검색 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="request" %}
{% code title="match 쿼리로 amazon 검색" %}

```javascript
GET GET my_synonym/_search
{
  "query": {
    "match": {
      "message": "amazon"
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="match 쿼리로 amazon 검색 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.22920427,
    "hits" : [
      {
        "_index" : "my_synonym",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.22920427,
        "_source" : {
          "message" : "AWS"
        }
      },
      {
        "_index" : "my_synonym",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.1513613,
        "_source" : {
          "message" : "Amazon Web Service"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 첫 번째 쿼리와 마지막 세 번째 쿼리의 결과가 동일합니다.

&#x20; **term** 쿼리는 검색어에 애널라이저를 적용하지 않고 그대로 검색하기 때문에 **term** 쿼리로 **aws** 를 검색하면 두개의 도큐먼트가 모두 검색되고 **amazon**을 검색 하면 검색이 되지 않습니다. **match** 쿼리는 검색어 **amazon**도 **my\_syn** 애널라이저가 적용이 되어 **aws** 로 변환하여 검색을 하기 때문에 **aws**로 검색을 한 것과 같은 결과가 나타납니다.

&#x20; 이번에는 **my\_synonym** 인덱스에 `"amazon, aws"`  로 동의어를 지정하는 예제입니다. 기존의 **my\_synonym** 인덱스를 먼저 삭제하고 입력합니다.

{% code title=""amazon, aws" 동의어를 지정하는 my\_synonym 인덱스 생성" %}

```javascript
PUT my_synonym
{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_syn": {
          "tokenizer": "whitespace",
          "filter": [
            "lowercase",
            "syn_aws"
          ]
        }
      },
      "filter": {
        "syn_aws": {
          "type": "synonym",
          "synonyms": [
            "amazon, aws"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "message": {
        "type": "text",
        "analyzer": "my_syn"
      }
    }
  }
}
```

{% endcode %}

&#x20; 앞의 예제와 동일하게 **"Amazon Web Service"**, **"AWS"** 값을 가진 도큐먼트 두 개를 저장하고 각 도큐먼트의 `_termvectors` 를 확인 해 보겠습니다.

{% code title="AWS, Amazon Web Service 도큐먼트 저장" %}

```javascript
PUT my_synonym/_doc/1
{ "message" : "Amazon Web Service" }
PUT my_synonym/_doc/2
{ "message" : "AWS" }
```

{% endcode %}

{% tabs %}
{% tab title="request" %}
{% code title="1 도큐먼트 message 필드의 termvectors 확인" %}

```javascript
GET my_synonym/_termvectors/1?fields=message
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="1 도큐먼트 message 필드의 termvectors 확인 결과" %}

```javascript
{
  "_index" : "my_synonym",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "took" : 0,
  "term_vectors" : {
    "message" : {
      "field_statistics" : {
        "sum_doc_freq" : 6,
        "doc_count" : 2,
        "sum_ttf" : 6
      },
      "terms" : {
        "amazon" : {
          "term_freq" : 1,
          "tokens" : [
            {
              "position" : 0,
              "start_offset" : 0,
              "end_offset" : 6
            }
          ]
        },
        "aws" : {
          "term_freq" : 1,
          "tokens" : [
            {
              "position" : 0,
              "start_offset" : 0,
              "end_offset" : 6
            }
          ]
        },
        "service" : {
          "term_freq" : 1,
          "tokens" : [
            {
              "position" : 2,
              "start_offset" : 11,
              "end_offset" : 18
            }
          ]
        },
        "web" : {
          "term_freq" : 1,
          "tokens" : [
            {
              "position" : 1,
              "start_offset" : 7,
              "end_offset" : 10
            }
          ]
        }
      }
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="request" %}
{% code title="2 도큐먼트 message 필드의 termvectors 확인" %}

```javascript
GET my_synonym/_termvectors/2?fields=message
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="2 도큐먼트 message 필드의 termvectors 확인 결과" %}

```javascript
{
  "_index" : "my_synonym",
  "_type" : "_doc",
  "_id" : "2",
  "_version" : 1,
  "found" : true,
  "took" : 0,
  "term_vectors" : {
    "message" : {
      "field_statistics" : {
        "sum_doc_freq" : 6,
        "doc_count" : 2,
        "sum_ttf" : 6
      },
      "terms" : {
        "amazon" : {
          "term_freq" : 1,
          "tokens" : [
            {
              "position" : 0,
              "start_offset" : 0,
              "end_offset" : 3
            }
          ]
        },
        "aws" : {
          "term_freq" : 1,
          "tokens" : [
            {
              "position" : 0,
              "start_offset" : 0,
              "end_offset" : 3
            }
          ]
        }
      }
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 두 도큐먼트 모두 `"position" : 0` 위치에 **"aws"**, **"amazon"** 두개의 텀들이 모두 저장된 것을 확인할 수 있습니다. 이제 **term** 쿼리로 **amazon** 을 검색해도 두개 도큐먼트가 모두 검색이 됩니다. 이것은 한번 직접 실행 해 보시기 바랍니다.

&#x20; 동의어 여러 개를 입력 할 때는 `"synonyms": [ ... ]` 항목 안에 배열로 넣어도 되지만, 그 보다는 파일을 따로 만들어 관리하는 것이 편합니다. **stop** 토큰 필터와 마찬가지로 **synonyms\_path** 항목에 **config** 디렉토리 기준의 상대 경로에 파일을 저장하고 경로명을 입력하면 됩니다. 동의어는 하나의 규칙당 **한 줄씩** 입력해야 하며 파일은 **UTF-8**로 인코딩 되어야 합니다.

&#x20; 다음은 **"hop, jump"**, **"quick, fast"** 를 **user\_dic/my\_syn\_dic.txt** 에 저장해서 동의어 사전으로 사용하는 예제입니다.

{% code title="config/user\_dic 디렉토리 아래에 my\_syn\_dic.txt 파일 생성" %}

```bash
$ echo 'quick, fast
hop, jump' > config/user_dic/my_syn_dic.txt
```

{% endcode %}

{% code title="synonyms\_path 설정을 가진 my\_synonym 인덱스 생성" %}

```javascript
PUT my_synonym
{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_syn": {
          "tokenizer": "whitespace",
          "filter": [
            "lowercase",
            "syn_aws"
          ]
        }
      },
      "filter": {
        "syn_aws": {
          "type": "synonym",
          "synonyms_path": "user_dic/my_syn_dic.txt"
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "message": {
        "type": "text",
        "analyzer": "my_syn"
      }
    }
  }
}
```

{% endcode %}

&#x20; 이제 **term** 쿼리로 **quick** 과 **jump**를 검색하면 **hop**, **fast** 도 검색이 됩니다.

{% code title="quick, jump, hop, fast 를 포함하는 도큐먼트 저장" %}

```javascript
PUT my_synonym/_doc/1
{ "message": "Quick brown fox jump" }
PUT my_synonym/_doc/2
{ "message": "hop rabbit is fast" }
```

{% endcode %}

{% tabs %}
{% tab title="request" %}
{% code title="term 쿼리로 quick, jump 검색" %}

```javascript
GET GET my_synonym/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "message": "quick"
          }
        },
        {
          "term": {
            "message": "jump"
          }
        }
      ]
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="term 쿼리로 quick, jump 검색 결과" %}

```javascript
{
  "took" : 370,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.42221838,
    "hits" : [
      {
        "_index" : "my_synonym",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.42221838,
        "_source" : {
          "message" : "Quick brown fox jump"
        }
      },
      {
        "_index" : "my_synonym",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.42221838,
        "_source" : {
          "message" : "hop rabbit is fast"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 마지막으로 **synonym** 토큰 필터에는 추가적으로 다음과 같은 옵션들이 있습니다.

* **expand** (true / false. 디폴트는 **true**)

  `"expand": false` 로 설정하게 되면 `"synonyms": "aws, amazon"` 같은 설정에 토큰들을 모두 저장하지 않고 맨 처음에 명시된 토큰 하나만 저장합니다. 앞의 설정은 `"synonyms": "aws, amazon => aws"` 로 설정한 것과 동일하게 동작합니다.
* **lenient** (true / false. 디폴트는 **false**)

  `"lenient": true` 로 설정하면 synonym 설정에 오류가 있는 경우 오류가 있는 부분을 무시하고 실행합니다.

&#x20;&#x20;


# 6.6.4 NGram, Edge NGram, Shingle

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

### NGram

&#x20; Elasticsearch는 빠른 검색을 위해 검색에 사용될 텀 들을 미리 분리해서 역 인덱스에 저장합니다. 하지만 과학 용어집 검색 같은 특정한 사용 사례에 따라 텀이 아닌 단어의 일부만 가지고도 검색해야 하는 기능이 필요한 경우도 있습니다. RDBMS의 LIKE 검색 처럼 사용하는 **wildcard** 쿼리나 **regexp (정규식)** 쿼리도 지원을 하지만, 이런 쿼리들은 메모리 소모가 많고 느리기 때문에 Elasticsearch의 장점을 활용하지 못합니다. 이런 사용을 위해 검색 텀의 일부만 미리 분리해서 저장을 할 수 있는데 이렇게 단어의 일부를 나눈 부위를 **NGram** 이라고 합니다. 보통은 **unigram**(유니그램 – 1글자), **bigram**(바이그램 - 2자) 등으로 부릅니다.

&#x20; Elasticsearch는 **NGram**을 처리하는 토큰 필터를 제공하며 설정은 `"type": "nGram"` 으로 합니다. **"house"** 라는 단어를 2 글자의 NGram (bigram) 으로 처리하면 다음과 같이 "**ho"**, **"ou"**, **"us"**, **"se"** 총 4개의 토큰들이 추출됩니다. ngram 토큰필터를 사용하면 이렇게 2글자씩 추출된 텀들이 모두 검색 토큰으로 저장됩니다. 이제 이 인덱스의 경우에는 검색어를 **"ho"** 라고만 검색을 해도 **ho**use 가 포함된 도큐먼트들이 검색이 됩니다.

![](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LoXjtYvigzLIFFxzH3r%2F-LoXlJTg2HGLEQQeeTHW%2F6.6.4-01.png?alt=media\&token=a6c7729f-6bca-413b-8d2e-d45da7f53cdf)

{% hint style="danger" %}
**ngram** 토큰필터를 사용하면 저장되는 텀의 갯수도 기하급수적으로 늘어나고 검색어를 **"ho"**&#xB85C; 검색 했을 때 **ho**use, s**ho**es 처럼 검색 결과를 예상하기 어렵기 때문에 일반적인 텍스트 검색에는 사용하지 않는 것이 좋습니다. ngram을 사용하기 적합한 사례는 카테고리 목록이나 태그 목록과 같이 전체 개수가 많지 않은 데이터 집단에 **자동완성** 같은 기능을 구현하는 데에 적합합니다.
{% endhint %}

&#x20; ngram 토큰 필터에는 `min_gram` (디폴트 1), `max_gram` (디폴트 2) 옵션이 있습니다. 짐작할 수 있듯이 최소, 최대 문자수의 토큰을 구분하는 단위입니다. **house**를 `"min_gram": 2`, `"max_gram": 3` 으로 설정하면 다음과 같이 분석되어 총 7개의 토큰을 저장합니다.

![](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LoXpIdpkquqZHUT7qh8%2F-LoXoG1F79BlxCT-CGXe%2F6.6.4-02.png?alt=media\&token=52c8e573-02ad-44ff-9901-b7a4a1823f80)

&#x20; 다음은 **my\_ngram** 인덱스에 `"min_gram": 2`, `"max_gram": 3` 인 **my\_ngram\_f** 토큰필터를 만들고 house 를 분석하는 예제입니다.

{% code title="my\_ngram 인덱스 생성" %}

```javascript
PUT my_ngram
{
  "settings": {
    "analysis": {
      "filter": {
        "my_ngram_f": {
          "type": "nGram",
          "min_gram": 2,
          "max_gram": 3
        }
      }
    }
  }
}
```

{% endcode %}

{% tabs %}
{% tab title="request" %}
{% code title="my\_ngram\_f 토큰필터로 "house" 분석" %}

```javascript
GET my_ngram/_analyze
{
  "tokenizer": "keyword",
  "filter": [
    "my_ngram_f"
  ],
  "text": "house"
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="my\_ngram\_f 토큰필터로 "house" 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "ho",
      "start_offset" : 0,
      "end_offset" : 5,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "hou",
      "start_offset" : 0,
      "end_offset" : 5,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "ou",
      "start_offset" : 0,
      "end_offset" : 5,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "ous",
      "start_offset" : 0,
      "end_offset" : 5,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "us",
      "start_offset" : 0,
      "end_offset" : 5,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "use",
      "start_offset" : 0,
      "end_offset" : 5,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "se",
      "start_offset" : 0,
      "end_offset" : 5,
      "type" : "word",
      "position" : 0
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

### Edge NGram

&#x20; 검색을 위해 NGram을 저장하더라도 보통은 단어의 맨 앞에서부터 검색하는 경우가 많습니다. 텀 앞쪽의 ngram 만 저장하기 위해서는 **Edge NGram** 토큰필터를 이용합니다. 설정 방법은 `"type": "edgeNGram"` 입니다. edgeNGram의 옵션을 `"min_gram": 1`, `"max_gram": 4` 으로 설정하고 "**house"** 를 분석하면 다음과 같이 **"h", "ho", "hou", "hous"** 4개의 토큰이 생성됩니다.

![](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LoXpIdpkquqZHUT7qh8%2F-LoXtTEnS0W5ydwG50v8%2F6.6.4-03.png?alt=media\&token=6efbf13a-6e3b-401a-86be-641aed6d8255)

&#x20; 인덱스 설정과 쿼리를 이용한 애널라이저 분석은 위의 NGram 예제를 참고해서 직접 만들어 보시기 바랍니다.

### Shingle

&#x20; **NGram**과 **Edge NGram**은 모두 하나의 단어로부터 토큰을 확장하는 토큰 필터입니다. 문자가 아니라 단어 단위로 구성된 묶음을 **Shingle** 이라고 하며 `"type": "shingle"` 토큰 필터의 이용이 가능합니다. **"this is my sweet home"** 라는 문장을 분리해서 **2 단어씩 Shingle** 토큰 필터를 적용하면 다음과 같은 4개의 **shingle** 들이 생성됩니다.

![](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LoXpIdpkquqZHUT7qh8%2F-LoXvCvpweKr0eFevUmh%2F6.6.4-04.png?alt=media\&token=770f4ef1-79c2-47b2-a275-8c9cfd734ea3)

&#x20; **Shingle** 토큰 필터에서 사용 가능한 옵션은 다음과 같습니다.

* **min\_shingle\_size** / **max\_shingle\_size** : shingle의 최소 / 최대 단어 개수를 지정합니다. 디폴트는 모두 2 입니다.
* **output\_unigrams** : Shingle 외에도 각각의 개별 토큰(unigram)도 저장 하는지의 여부를 설정합니다. 디폴트는 true 입니다.
* **output\_unigrams\_if\_no\_shingles** : shingle 을 만들 수 없는 경우에만 개별 토큰을 저장하는지의 여부를 설정합니다. 디폴트는 false 입니다.
* **token\_separator** : 토큰 구분자를 지정합니다. 디폴트는 `" "` (스페이스) 입니다.
* **filler\_token** : shing을 만들 텀이 없는 경우 (보통은 stop 토큰 필터와 함께 사용되어 offset 위치만 있고 텀이 없는 경우입니다) 대체할 텍스트를 지정합니다. 디폴트는 `_` 입니다.

&#x20; 다음은 **my\_shingle** 인덱스에서 `"min_shingle_size": 3`, `"max_shingle_size": 4` 로 설정해서 **"this is my sweet home"** 문장을 분석하는 예제입니다.

{% code title="my\_shingle 인덱스 생성" %}

```javascript
PUT my_shingle
{
  "settings": {
    "analysis": {
      "filter": {
        "my_shingle_f": {
          "type": "shingle",
          "min_shingle_size": 3,
          "max_shingle_size": 4
        }
      }
    }
  }
}
```

{% endcode %}

{% tabs %}
{% tab title="request" %}
{% code title="my\_shingle\_f 토큰필터로 "this is my sweet home" 분석" %}

```javascript
GET my_shingle/_analyze
{
  "tokenizer": "whitespace",
  "filter": [
    "my_shingle_f"
  ],
  "text": "this is my sweet home"
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="my\_shingle\_f 토큰필터로 "this is my sweet home" 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "this",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "this is my",
      "start_offset" : 0,
      "end_offset" : 10,
      "type" : "shingle",
      "position" : 0,
      "positionLength" : 3
    },
    {
      "token" : "this is my sweet",
      "start_offset" : 0,
      "end_offset" : 16,
      "type" : "shingle",
      "position" : 0,
      "positionLength" : 4
    },
    {
      "token" : "is",
      "start_offset" : 5,
      "end_offset" : 7,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "is my sweet",
      "start_offset" : 5,
      "end_offset" : 16,
      "type" : "shingle",
      "position" : 1,
      "positionLength" : 3
    },
    {
      "token" : "is my sweet home",
      "start_offset" : 5,
      "end_offset" : 21,
      "type" : "shingle",
      "position" : 1,
      "positionLength" : 4
    },
    {
      "token" : "my",
      "start_offset" : 8,
      "end_offset" : 10,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : "my sweet home",
      "start_offset" : 8,
      "end_offset" : 21,
      "type" : "shingle",
      "position" : 2,
      "positionLength" : 3
    },
    {
      "token" : "sweet",
      "start_offset" : 11,
      "end_offset" : 16,
      "type" : "word",
      "position" : 3
    },
    {
      "token" : "home",
      "start_offset" : 17,
      "end_offset" : 21,
      "type" : "word",
      "position" : 4
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; `"token" : "this is my"`, `"token" : "this is my sweet"` 와 같은 토큰들이 저장된 것을 확인할 수 있습니다. 이번에는 `"output_unigrams": false` 와 `"filler_token": "-"` 설정을 추가하고 stop 토큰필터로 `"is"` 를 불용어 처리한 뒤 실행 해 보겠습니다.

{% code title="my\_shingle 인덱스 생성" %}

```javascript
PUT my_shingle
{
  "settings": {
    "analysis": {
      "filter": {
        "my_shingle_f": {
          "type": "shingle",
          "min_shingle_size": 3,
          "max_shingle_size": 4,
          "output_unigrams": false,
          "filler_token": "-"
        },
        "my_stop_f": {
          "type": "stop",
          "stopwords": [
            "is"
          ]
        }
      }
    }
  }
}
```

{% endcode %}

{% tabs %}
{% tab title="request" %}
{% code title="my\_shingle 에서 "this is my sweet home" 분석" %}

```javascript
GET my_shingle/_analyze
{
  "tokenizer": "whitespace",
  "filter": [
    "my_stop_f",
    "my_shingle_f"
  ],
  "text": "this is my sweet home"
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="my\_shingle 에서 "this is my sweet home" 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "this - my",
      "start_offset" : 0,
      "end_offset" : 10,
      "type" : "shingle",
      "position" : 0
    },
    {
      "token" : "this - my sweet",
      "start_offset" : 0,
      "end_offset" : 16,
      "type" : "shingle",
      "position" : 0,
      "positionLength" : 2
    },
    {
      "token" : "- my sweet",
      "start_offset" : 8,
      "end_offset" : 16,
      "type" : "shingle",
      "position" : 1
    },
    {
      "token" : "- my sweet home",
      "start_offset" : 8,
      "end_offset" : 21,
      "type" : "shingle",
      "position" : 1,
      "positionLength" : 2
    },
    {
      "token" : "my sweet home",
      "start_offset" : 8,
      "end_offset" : 21,
      "type" : "shingle",
      "position" : 2
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 단일 토큰들은 모두 삭제되었고 `"is"` 는 `"-"` 로 대치된 3, 4개 단어로 이루어진 shingle 들이 생성된 것을 확인할 수 있습니다.

{% hint style="warning" %}
**NGram**, **Edged NGram** 그리고 **Shingle** 토큰 필터는 보통 일반적인 텍스트 분석에 사용하기는 적합하지 않습니다. 하지만 자동 완성 기능을 구현하거나 프로그램 코드 안에서 문법이나 기능명을 검색하는 것과 같이 특수한 요구사항을 충족해야 하는 경우 유용하게 사용될 수 있습니다.
{% endhint %}


# 6.6.5 Unique

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; **"white fox, white rabbit, white bear"** 같은 문장을 분석하면 **"white"** 텀은 총 3번 저장이 됩니다. 역 색인에는 텀이 1개만 있어도 텀을 포함하는 도큐먼트를 가져올 수 있기 때문에 중복되는 텀 들은 삭제 해 주어도 검색에는 보통 무방합니다. 이 경우 **unique** 토큰 필터를 사용해서 중복되는 텀 들은 하나만 저장하도록 할 수 있습니다.&#x20;

&#x20; 다음은 **"white fox, white rabbit, white bear"** 문장을 **unique** 토큰 필터를 적용하지 않은 경우와 적용한 경우를 비교 한 예제입니다.

{% tabs %}
{% tab title="request" %}
{% code title="일반적인 "white fox, white rabbit, white bear" 문장 분석" %}

```javascript
GET _analyze
{
  "tokenizer": "standard",
  "filter": [
    "lowercase"
  ],
  "text": [
    "white fox, white rabbit, white bear"
  ]
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="일반적인 "white fox, white rabbit, white bear" 문장 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "white",
      "start_offset" : 0,
      "end_offset" : 5,
      "type" : "<ALPHANUM>",
      "position" : 0
    },
    {
      "token" : "fox",
      "start_offset" : 6,
      "end_offset" : 9,
      "type" : "<ALPHANUM>",
      "position" : 1
    },
    {
      "token" : "white",
      "start_offset" : 11,
      "end_offset" : 16,
      "type" : "<ALPHANUM>",
      "position" : 2
    },
    {
      "token" : "rabbit",
      "start_offset" : 17,
      "end_offset" : 23,
      "type" : "<ALPHANUM>",
      "position" : 3
    },
    {
      "token" : "white",
      "start_offset" : 25,
      "end_offset" : 30,
      "type" : "<ALPHANUM>",
      "position" : 4
    },
    {
      "token" : "bear",
      "start_offset" : 31,
      "end_offset" : 35,
      "type" : "<ALPHANUM>",
      "position" : 5
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="request" %}
{% code title="unique 토크나이저로 "white fox, white rabbit, white bear" 문장 분석" %}

```javascript
GET _analyze
{
  "tokenizer": "standard",
  "filter": [
    "lowercase",
    "unique"
  ],
  "text": [
    "white fox, white rabbit, white bear"
  ]
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="unique 토크나이저로 "white fox, white rabbit, white bear" 문장 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "white",
      "start_offset" : 0,
      "end_offset" : 5,
      "type" : "<ALPHANUM>",
      "position" : 0
    },
    {
      "token" : "fox",
      "start_offset" : 6,
      "end_offset" : 9,
      "type" : "<ALPHANUM>",
      "position" : 1
    },
    {
      "token" : "rabbit",
      "start_offset" : 17,
      "end_offset" : 23,
      "type" : "<ALPHANUM>",
      "position" : 2
    },
    {
      "token" : "bear",
      "start_offset" : 31,
      "end_offset" : 35,
      "type" : "<ALPHANUM>",
      "position" : 3
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; `"unique"` 토큰 필터를 적용하면 **"white"** 텀이 하나만 저장된 것을 확인할 수 있습니다.

{% hint style="danger" %}
match 쿼리를 사용해서 검색하는 경우 unique 토큰 필터를 적용한 필드는 텀의 개수가 1개로 되기 때문에 **TF(Term Frequency)** 값이 줄어들어 스코어 점수가 달라질 수 있습니다. match 쿼리를 이용해 **정확도(relevancy)** 를 따져야 하는 검색의 경우에는 unique 토큰 필터는 사용하지 않는 것이 바람직합니다.
{% endhint %}


# 6.7 형태소 분석 - Stemming

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 우리가 사용하는 언어들은 영어만 보더라도 문법에 따라 명사 뒤에 **\~s**, **\~ness** 등이 붙거나 동사 뒤에 **\~ing**, **\~ed** 등이 붙는 등 변화가 많습니다. 검색을 할 때는 보통 이런 문법에 따른 단어의 변형에 상관 없이 검색이 가능해야 하기 때문에 텍스트 데이터를 분석할 때 각각의 텀에 있는 단어들을 기본 형태인 어간을 추출하는 과정을 진행해야 합니다. 이 과정을 보통 **어간 추출** 또는 **형태소 분석** 이라고 하며 영어로는 **stemming** 이라고 합니다. 그리고 형태소 분석을 하는 도구를 형태소 분석기, 영어로는 **stemmer** 라고 합니다.

&#x20; Elasticsearch 에서는 다양한 형태소 분석기들을 지원하며 Elastic사에서 공식적으로 지원하지 않는 국가의 언어들도 플러그인 형태로 사용 가능하도록 오픈소스로 배포되는 분석기들이 많이 있습니다. Elasticsearch 에서 사용 가능한 형태소 분석기 중에서 가장 많이 알려진 형태소 분석 알고리즘인 Snowball 과 한글 형태소 분석기인 Nori에 대해 살펴보도록 하겠습니다.


# 6.7.1 Snowball

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; **Snowball** 은 2000년 경에 정보 검색의 선구자인 마틴 포터 박사가 개발하여 무료로 공개한 형태소 분석 알고리즘입니다. 보통 \~ing, \~s 등을 제거하여 문장에 쓰인 단어들을 기본 형태로 변경합니다. Elasticsearch 에서 Snowball 은 애널라이저, 토크나이저, 토큰 필터가 모두 정의되어 있으며 사용 방법은 앞의 [사용자 정의 애널라이저](/06-text-analysis/6.3-analyzer-1/6.4-custom-analyzer) 부분에 설명되어 있으니 기억나지 않으면 앞으로 가서 다시 확인 해 보시기 바랍니다.


# 6.7.2 노리 (nori) 한글 형태소 분석기

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

### 커뮤니티 한글 형태소 분석기 - 아리랑, 은전한닢, Open Korean Text

&#x20; 한글은 형태의 변형이 매우 복잡한 언어입니다. 특히 복합어, 합성어 등이 많아 하나의 단어도 여러 어간으로 분리해야 하는 경우가 많아 한글을 형태소 분석을 하려면 반드시 한글 형태소 사전이 필요합니다. 오픈 소스 커뮤니티에서 개발되어 Elasticsearch에서 사용 가능한 한글 형태소 분석기는 다음과 같은 것들이 있습니다.

* **아리랑 (arirang)**
  * URL: <https://github.com/HowookJeong/elasticsearch-analysis-arirang>
  * 설명: korean analyzer (lucene analyzer kr arirang)
  * License: as-is
* **은전한닢 (seunjeon)**
  * URL: <https://bitbucket.org/eunjeon/seunjeon>
  * 설명: mecab-ko-dic 기반으로 만들어진 JVM 상에서 돌아가는 한국어 형태소분석기입니다. 기본적으로 java와 scala 인터페이스를 제공합니다. 사전이 패키지 내에 포함되어 있기 때문에 별도로 mecab-ko-dic을 설치할 필요가 없습니다. 특징으로는 (시스템 사전에 등록되어 있는 단어에 한하여) 복합명사 분해와 활용어 원형 찾기가 가능합니다.
  * License: Apache 2.0
* **Open Korean Text**&#x20;
  * URL: <https://github.com/open-korean-text/open-korean-text>
  * 설명: 오픈소스 한국어 처리기 (Official Fork of twitter-korean-text). 스칼라로 쓰여진 한국어 처리기입니다. 현재 텍스트 정규화와 형태소 분석, 스테밍을 지원하고 있습니다. 짧은 트윗은 물론이고 긴 글도 처리할 수 있습니다.
  * License: Apache 2.0

(<https://www.elastic.co/kr/blog/using-korean-analyzers> 에서 발췌)

&#x20; Elasticsearch가 한글을 지원하지 않던 시절에 위의 형태소 분석기들은 한글 사용자들에게 큰 도움이 되었습니다. 하지만 외부에서 만들어진 기능이다 보니 Elasticsearch 버전이 올라가 구조가 변경되면 사용이 불가능해지고, 버그가 오류가 있어도 누군가가 나서서 쉽게 고치기 어려운 문제가 있었습니다.

### Nori 개요

&#x20; Elasticsearch 6.6 버전 부터 공식적으로 **Nori(노리)** 라고 하는 한글 형태소 분석기를 Elastic사에서 공식적으로 개발해서 지원을 하기 시작했습니다. 특이하게 nori는 프랑스 엔지니어인 [Jim Ferenczi](https://github.com/jimczi) 에 의해 처음 개발이 되었습니다. Jim 은 아파치 루씬의 커미터이며 Elasticsearch의 일본어 형태소 분석기인 **Kuromoji(구로모지)** 역시 Jim 이 처음 개발했습니다. Nori 는 **은전한닢**에서 사용하는 **mecab-ko-dic** 사전을 재 가공 하여 사용하고 있습니다. Nori 는 루씬의 기능으로 개발되었으며 루씬 소스에 반영되어 있으며 개발 이력은\
<https://issues.apache.org/jira/browse/LUCENE-8231>\
에서 확인 할 수 있고 프로그램 소스는\
<https://github.com/apache/lucene-solr/tree/master/lucene/analysis/nori>\
에서 확인이 가능합니다.

&#x20; Nori 에 관련한 설명은 공식 홈페이지의 문서 페이지의 [Elasticsearch : Plugins and Integrations > Analysis Plugins > Nori](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-nori.html) 페이지에서 찾을 수 있습니다.

### Nori 설치

&#x20; Nori 를 사용하기 위해서는 먼저 elasticsearch에 analysis-nori 플러그인을 설치해야 합니다. elasticsearch 홈 디렉토리에서 다음 명령을 실행하면 버전에 맞는 nori 플러그인을 받아서 자동으로 설치합니다.

{% code title="nori 플러그인 설치" %}

```bash
$ bin/elasticsearch-plugin install analysis-nori
```

{% endcode %}

&#x20; 설치된 nori 플러그인을 제거하려면 다음 명령을 실행합니다.

{% code title="nori 플러그인 제거" %}

```bash
$ bin/elasticsearch-plugin remove analysis-nori
```

{% endcode %}

[Elastic 클라우드 서비스](https://cloud.elastic.co/)에서 사용하기 위해서는 클러스터를 배포할 때 Customize deployment 메뉴의 Manage plugins and settings 부분에서 analysis-nori 부분을 선택합니다.

![Elastic Cloud 서비스에서 nori 설치](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LobgPxlMaOw0_GcJLsy%2F-Lobi38ViQ3_K63cl6Ya%2F6.7.2-01.png?alt=media\&token=ed9d2451-1982-4cb2-a02b-14eefd217bb3)

### nori\_tokenizer

&#x20; Nori는 **nori\_tokenizer** 토크나이저와 **nori\_part\_of\_speech**, **nori\_readingform** 토큰 필터를 제공합니다. 먼저 nori\_tokenizer 토크나이저를 사용해서 한글을 간단하게 테스트 할 수 있습니다. 다음은 standard와 nori\_tokenizer 를 비교해서 **"동해물과 백두산이"** 를 분석한 예제입니다. 당연히 테스트 하는 elasticsearch 에는 analysis-nori 플러그인이 설치되어 있어야 합니다.

{% tabs %}
{% tab title="request" %}
{% code title="standard 토크나이저로 "동해물과 백두산이" 문장 분석" %}

```javascript
GET _analyze
{
  "tokenizer": "standard",
  "text": [
    "동해물과 백두산이"
  ]
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="standard 토크나이저로 "동해물과 백두산이" 문장 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "동해물과",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "<HANGUL>",
      "position" : 0
    },
    {
      "token" : "백두산이",
      "start_offset" : 5,
      "end_offset" : 9,
      "type" : "<HANGUL>",
      "position" : 1
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="request" %}
{% code title="nori\_tokenizer 토크나이저로 "동해물과 백두산이" 문장 분석" %}

```javascript
GET _analyze
{
  "tokenizer": "nori_tokenizer",
  "text": [
    "동해물과 백두산이"
  ]
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="nori\_tokenizer 토크나이저로 "동해물과 백두산이" 문장 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "동해",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "물",
      "start_offset" : 2,
      "end_offset" : 3,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "과",
      "start_offset" : 3,
      "end_offset" : 4,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : "백두",
      "start_offset" : 5,
      "end_offset" : 7,
      "type" : "word",
      "position" : 3
    },
    {
      "token" : "산",
      "start_offset" : 7,
      "end_offset" : 8,
      "type" : "word",
      "position" : 4
    },
    {
      "token" : "이",
      "start_offset" : 8,
      "end_offset" : 9,
      "type" : "word",
      "position" : 5
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; Standard 토크나이저는 공백 외에 아무런 분리를 하지 못했지만 nori\_tokenizer는 한국어 사전 정보를 이용해 `"token" : "동해"`, `"token" : "산"` 같은 단어을 분리 한 것을 확인할 수 있습니다. nori\_tokenizer 에는 다음과 같은 옵션들이 있습니다.

* **user\_dictionary** : 사용자 사전이 저장된 파일의 경로를 입력합니다.
* **user\_dictionary\_rules** : 사용자 정의 사전을 배열로 입력합니다.
* **decompound\_mode** : 합성어의 저장 방식을 결정합니다. 다음 3개의 값을 사용 가능합니다.
  * `none` : 어근을 분리하지 않고 완성된 합성어만 저장합니다.
  * `discard` (디폴트) : 합성어를 분리하여 각 어근만 저장합니다.
  * `mixed` : 어근과 합성어를 모두 저장합니다.

&#x20; **user\_dictionary**는 다른 애널라이저들과 마찬가지로 config 디렉토리의 상대 경로를 입력하며 변경시 인덱스를 \_close / \_open 하면 반영됩니다. 사전의 단어들에는 우선순위가 있으며 문장 **"동해물과"** 에서는 **"동해"** 가 가장 우선순위가 높아 "동해" 가 먼저 추출되고 다시 **"물"** 그리고 **"과"** 가 추출되어 **"동해"+"물"+"과"** 같은 형태가 됩니다. user\_dictionary 경로에 있는 사전 파일이나 user\_dictionary\_rules 설정값에 단어만 나열 해 주면 이 단어들을 가장 우선으로 추출합니다.

&#x20; 다음은 **my\_nori** 인덱스에 **user\_dictionary\_rules**옵션을 이용하여 사용자 사전 **"해물"** 을 지정하고 **"동해물과"** 를 분석한 예제입니다.

{% code title="my\_nori 인덱스에 "해물" 사전을 추가한 my\_nori\_tokenizer 생성" %}

```javascript
PUT my_nori
{
  "settings": {
    "analysis": {
      "tokenizer": {
        "my_nori_tokenizer": {
          "type": "nori_tokenizer",
          "user_dictionary_rules": [
            "해물"
          ]
        }
      }
    }
  }
}
```

{% endcode %}

{% tabs %}
{% tab title="request" %}
{% code title="my\_nori\_tokenizer 토크나이저로 "동해물과" 분석" %}

```javascript
GET my_nori/_analyze
{
  "tokenizer": "my_nori_tokenizer",
  "text": [
    "동해물과"
  ]
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="my\_nori\_tokenizer 토크나이저로 "동해물과" 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "동",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "해물",
      "start_offset" : 1,
      "end_offset" : 3,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "과",
      "start_offset" : 3,
      "end_offset" : 4,
      "type" : "word",
      "position" : 2
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 이렇게 사용자 사전에 **"해물"** 이라는 단어를 추가하면 "동해물과" 는 **"동"+"해물"+"과"** 로 분석이 되어 이 문장이 포함된 도큐먼트는 "동해" 로는 검색이 되지 않고 **"해물"**&#xB85C; 검색이 됩니다.

&#x20; "백두산" 은 "백두"+"산" 두 어근이 합쳐진 합성어 입니다. 보통 "미역"+"국" 같은 음식이나 "서울"+"역" 같은 역 이름에 합성어가 많습니다. 다음은 **decompound\_mode** 의 3가지 옵션이 문장 "백두산이"을 각각 어떻게 분석되는지 확인하는 예제입니다.

{% code title="decompound\_mode 모드를 각각 none, discard, mixed 로 설정한 토크나이저 설장" %}

```javascript
PUT my_nori
{
  "settings": {
    "analysis": {
      "tokenizer": {
        "nori_none": {
          "type": "nori_tokenizer",
          "decompound_mode": "none"
        },
        "nori_discard": {
          "type": "nori_tokenizer",
          "decompound_mode": "discard"
        },
        "nori_mixed": {
          "type": "nori_tokenizer",
          "decompound_mode": "mixed"
        }
      }
    }
  }
}
```

{% endcode %}

{% tabs %}
{% tab title="request" %}
{% code title="nori\_none 토크나이저로 "백두산이" 분석" %}

```javascript
GET my_nori/_analyze
{
  "tokenizer": "nori_none",
  "text": [ "백두산이" ]
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="nori\_none 토크나이저로 "백두산이" 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "백두산",
      "start_offset" : 0,
      "end_offset" : 3,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "이",
      "start_offset" : 3,
      "end_offset" : 4,
      "type" : "word",
      "position" : 1
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="request" %}
{% code title="nori\_discard 토크나이저로 "백두산이" 분석" %}

```javascript
GET my_nori/_analyze
{
  "tokenizer": "nori_discard",
  "text": [ "백두산이" ]
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="nori\_discard 토크나이저로 "백두산이" 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "백두",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "산",
      "start_offset" : 2,
      "end_offset" : 3,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "이",
      "start_offset" : 3,
      "end_offset" : 4,
      "type" : "word",
      "position" : 2
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="request" %}
{% code title="nori\_mixed 토크나이저로 "백두산이" 분석" %}

```javascript
GET my_nori/_analyze
{
  "tokenizer": "nori_mixed",
  "text": [ "백두산이" ]
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="nori\_mixed 토크나이저로 "백두산이" 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "백두산",
      "start_offset" : 0,
      "end_offset" : 3,
      "type" : "word",
      "position" : 0,
      "positionLength" : 2
    },
    {
      "token" : "백두",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "산",
      "start_offset" : 2,
      "end_offset" : 3,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "이",
      "start_offset" : 3,
      "end_offset" : 4,
      "type" : "word",
      "position" : 2
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 각 설정에 따라 어근을 분리하거나 분리하지 않거나 모두 저장하는 것을 확인할 수 있습니다. **decompound\_mode** 의 디폴트 값은 **discard** 입니다.

### nori\_part\_of\_speech 와 품사 정보

&#x20; 한글 검색에서는 보통 명사, 동명사 정도만을 검색하고 조사, 형용사 등은 제거하는 것이 바람직합니다. **nori\_part\_of\_speech** 토큰 필터를 이용해서 제거할 **품사(POS - Part Of Speech)** 정보의 지정이 가능하며, 옵션 **stoptags** 값에 배열로 제외할 품사 코드를 나열해서 입력해서 사용합니다. 다음은 품사 코드의 일부 정보들입니다.

![(출처 : 꼬꼬마 한국어 형태소 분석기 - http://kkma.snu.ac.kr/documents/?doc=postag)](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LoinpqY1xA7ock1sc6i%2F-Loioly2sAhomKoXMv2-%2F6.7.2-02.png?alt=media\&token=47fae11e-c38e-4dff-92e8-64515e37f565)

&#x20; 이 외의 품사 코드는 출처에 명시된 정보 페이지에서 찾을 수 있습니다. **stoptags**의 디폴트 값은 다음과 같습니다.

{% code title="stoptags 디폴트 값" %}

```javascript
"stoptags": [
  "E", "IC", "J", "MAG", "MAJ",
  "MM", "SP", "SSC", "SSO", "SC",
  "SE", "XPN", "XSA", "XSN", "XSV",
  "UNA", "NA", "VSV"
]
```

{% endcode %}

&#x20; 다음은 **my\_pos** 인덱스에 **수사(NR)**&#xB97C; 제거하도록 **stoptags**를 지정하고 문장 **"다섯아이가"**&#xB97C; 분석한 예제입니다.

{% code title="my\_pos 인덱스에 수사(NR)을 제거하는 my\_pos\_f 토큰필터 지정" %}

```javascript
PUT my_pos
{
  "settings": {
    "index": {
      "analysis": {
        "filter": {
          "my_pos_f": {
            "type": "nori_part_of_speech",
            "stoptags": [
              "NR"
            ]
          }
        }
      }
    }
  }
}
```

{% endcode %}

{% tabs %}
{% tab title="request" %}
{% code title="my\_pos\_f 토큰필터로 "다섯아이가" 분석" %}

```javascript
GET my_pos/_analyze
{
  "tokenizer": "nori_tokenizer",
  "filter": [
    "my_pos_f"
  ],
  "text": "다섯아이가"
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="my\_pos\_f 토큰필터로 "다섯아이가" 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "아이",
      "start_offset" : 2,
      "end_offset" : 4,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "가",
      "start_offset" : 4,
      "end_offset" : 5,
      "type" : "word",
      "position" : 2
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 본래 **"다섯"+"아이"+"가"** 로 분석되어야 할 문장에서 수사인 **"다섯"**&#xC774; 제거 된 것을 확인할 수 있습니다.&#x20;

### nori\_readingform

&#x20; **nori\_readingform** 토큰 필터는 한자로 된 단어를 한글로 바꾸어 저장을 합니다. 별도의 옵션 없이 토큰필터로 명시하면 바로 적용이 가능합니다. 다음은 "春夏秋冬"(춘하추동)을 nori\_readingform 토큰 필터를 사용해서 한글로 변형하는 예제입니다.

{% tabs %}
{% tab title="request" %}
{% code title="nori\_readingform 토큰필터로 "春夏秋冬"(춘하추동) 분석" %}

```javascript
GET _analyze
{
  "tokenizer": "nori_tokenizer",
  "filter": [
    "nori_readingform"
  ],
  "text": "春夏秋冬"
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="nori\_readingform 토큰필터로 "春夏秋冬"(춘하추동) 분석 결과" %}

```javascript
{
  "tokens" : [
    {
      "token" : "춘하추동",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "word",
      "position" : 0
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

### explain : true 옵션

&#x20; query 또는 \_analuze API 에서 `"explain": true` 옵션을 추가하면 분석된 한글 형태소들의 품사 정보를 같이 볼 수 있습니다. **explain** 옵션은 nori 외에도 대부분의 애널라이저나 쿼리에서 사용하면 확장된 정보를 보여줍니다. 다음은 **"동해물과 백두산이"** 문장을 분석하면서 explain 옵션을 추가하여 상세 정보를 본 예제입니다.

{% tabs %}
{% tab title="request" %}
{% code title=""explain": true 옵션을 이용해서 분석 정보 표시" %}

```javascript
GET _analyze
{
  "tokenizer": "nori_tokenizer",
  "text": "동해물과 백두산이",
  "explain": true
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title=""explain": true 옵션을 이용해서 분석 정보 표시 결과" %}

```javascript
{
  "detail" : {
    "custom_analyzer" : true,
    "charfilters" : [ ],
    "tokenizer" : {
      "name" : "nori_tokenizer",
      "tokens" : [
        {
          "token" : "동해",
          "start_offset" : 0,
          "end_offset" : 2,
          "type" : "word",
          "position" : 0,
          "bytes" : "[eb 8f 99 ed 95 b4]",
          "leftPOS" : "NNP(Proper Noun)",
          "morphemes" : null,
          "posType" : "MORPHEME",
          "positionLength" : 1,
          "reading" : null,
          "rightPOS" : "NNP(Proper Noun)",
          "termFrequency" : 1
        },
        {
          "token" : "물",
          "start_offset" : 2,
          "end_offset" : 3,
          "type" : "word",
          "position" : 1,
          "bytes" : "[eb ac bc]",
          "leftPOS" : "NNG(General Noun)",
          "morphemes" : null,
          "posType" : "MORPHEME",
          "positionLength" : 1,
          "reading" : null,
          "rightPOS" : "NNG(General Noun)",
          "termFrequency" : 1
        },
        {
          "token" : "과",
          "start_offset" : 3,
          "end_offset" : 4,
          "type" : "word",
          "position" : 2,
          "bytes" : "[ea b3 bc]",
          "leftPOS" : "J(Ending Particle)",
          "morphemes" : null,
          "posType" : "MORPHEME",
          "positionLength" : 1,
          "reading" : null,
          "rightPOS" : "J(Ending Particle)",
          "termFrequency" : 1
        },
        {
          "token" : "백두",
          "start_offset" : 5,
          "end_offset" : 7,
          "type" : "word",
          "position" : 3,
          "bytes" : "[eb b0 b1 eb 91 90]",
          "leftPOS" : "NNG(General Noun)",
          "morphemes" : null,
          "posType" : "MORPHEME",
          "positionLength" : 1,
          "reading" : null,
          "rightPOS" : "NNG(General Noun)",
          "termFrequency" : 1
        },
        {
          "token" : "산",
          "start_offset" : 7,
          "end_offset" : 8,
          "type" : "word",
          "position" : 4,
          "bytes" : "[ec 82 b0]",
          "leftPOS" : "NNG(General Noun)",
          "morphemes" : null,
          "posType" : "MORPHEME",
          "positionLength" : 1,
          "reading" : null,
          "rightPOS" : "NNG(General Noun)",
          "termFrequency" : 1
        },
        {
          "token" : "이",
          "start_offset" : 8,
          "end_offset" : 9,
          "type" : "word",
          "position" : 5,
          "bytes" : "[ec 9d b4]",
          "leftPOS" : "J(Ending Particle)",
          "morphemes" : null,
          "posType" : "MORPHEME",
          "positionLength" : 1,
          "reading" : null,
          "rightPOS" : "J(Ending Particle)",
          "termFrequency" : 1
        }
      ]
    },
    "tokenfilters" : [ ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 이번 장에서는 elasticsearch가 데이터를 저장하는 색인 과정에서 처리하는 수많은 작업들에 대해 알아보았습니다. 텍스트 분석 및 텀의 개념과, 데이터 분석에 사용되는 애널라이저, 토크나이저, 토큰 필터, 캐릭터 필터 도구들에 대해 학습을 했습니다. 이런 텍스트 데이터 처리 과정을 통해 Elasticsearch는 빠른 풀 텍스트 검색 기능을 제공하며 다양한 방법으로 데이터를 다룰 수 있도록 합니다.

&#x20; 다음 장에서는 인덱스의 세팅 및 매핑 설정 방법과 다양한 형태의 필드들에 대해 알아보도록 하겠습니다.


# 7. 인덱스 설정과 매핑 - Settings & Mappings

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Elasticsearch 의 **인덱스**는 도큐먼트들이 모여 있는 논리적인 데이터의 집합입니다. [3장](/03-cluster/3.2-index-and-shards)에서 언급했듯이 인덱스는 하나의 노드에만 존재하지 않고 샤드 단위로 구분되어 여러 노드에 걸쳐 저장되어 데이터 무결성의 보장과 검색 성능의 향상을 실현합니다. 그 밖에도 데이터의 저장 및 검색 방법에 대한 설정이나 앞 장에서 살펴본 사용자 정의 애널라이저 같은 도구들은 대부 인덱스 단위로 구분되어 저장이 됩니다. 다시 말해 한 인덱스에서 사용되는 설정이나 도구들은 다른 인덱스에 영향을 미치지 않습니다. 이번 장에서는 인덱스의 단위에서 이루어지는 설정들과 데이터 명세인 인덱스 매핑에 대해 알아보도록 하겠습니다.


# 7.1 설정 - Settings

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 모든 인덱스는 두 개의 정보 단위를 가지고 있는데 바로 **settings** 과 **mappings** 입니다. 인덱스를 처음 생성한 뒤 `GET <인덱스명>` 으로 조회하면 설정(settings) 그리고 매핑(mappings) 정보를 확인할 수 있습니다.

{% code title="my\_index 인덱스 생성" %}

```javascript
PUT my_index
```

{% endcode %}

{% tabs %}
{% tab title="request" %}
{% code title="my\_index 인덱스의 settings, mappings 확인" %}

```javascript
GET my_index
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="my\_index 인덱스의 settings, mappings 확인 결과" %}

```javascript
{
  "my_index" : {
    "aliases" : { },
    "mappings" : { },
    "settings" : {
      "index" : {
        "creation_date" : "1568695052917",
        "number_of_shards" : "1",
        "number_of_replicas" : "1",
        "uuid" : "Ol2vvLbgSfiJcjDC0Eo85A",
        "version" : {
          "created" : "7030099"
        },
        "provided_name" : "my_index"
      }
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; settings 또는 mappings 정보만 따로 보고 싶으면 `GET my_index/_settings` 처럼 인덱스명 뒤에 **\_settings** 또는 **\_mappings** 를 추가해서 볼 수 있습니다. 처음 인덱스를 정의하면 몇가지 정보들이 자동으로 생성이 되는데, 샤드 수(number\_of\_shards) 나 복제본 수(number\_of\_replicas) 같은 정보는 settings 아래 설정됩니다. 샤드 수는 6.x 버전 까지는 디폴트로 5개가 설정되었고, 7.0 부터는 디폴트 1개로 설정이 됩니다.

&#x20; 위 예제는 `PUT my_index` 명령으로 인덱스만 생성하고 데이터는 입력하지 않았기 때문에 `"mappings" : { }` 정보는 아직 생성되지 않았습니다. 기본 설정들은 인덱스를 처음 생성할 때 명시합니다. 어떤 설정들은 운영 도중에도 바꿀 수 있지만 대부분의 설정들은 한번 지정하면 변경이 되지 않는 경우가 많습니다.

### number\_of\_shards, number\_of\_replicas&#x20;

&#x20; 먼저 앞에서 여러 번 이미 언급을 했는데, 프라이머리 샤드 수와 리플리카는 다음과 같이 각각 **number\_of\_shards**, **number\_of\_replicas** 에서 설정합니다. 대부분의 설정들은 **settings** 아래의 **index** 아래 설정에 명시되는데 **index** 레벨은 생략하고 입력하여도 정상적으로 입력이 됩니다. 입력하는 JSON 설정값은 { } 내부에 써도 되고 마침표 `.`를 이용해서 구조를 정의하는것도 가능합니다. 다음 명령들은 모두 동일하게 동작합니다.

{% code title="my\_index 인덱스 생성 - 괄호 { } 안에 하위 값 지정" %}

```javascript
PUT my_index
{
  "settings": {
    "index": {
      "number_of_shards": 3,
      "number_of_replicas": 1
    }
  }
}
```

{% endcode %}

{% code title="my\_index 인덱스 생성 - 마침표 . 으로 하위 값 지정" %}

```javascript
PUT my_index
{
  "settings": {
    "index.number_of_shards": 3,
    "index.number_of_replicas": 1
  }
}
```

{% endcode %}

{% code title="my\_index 인덱스 생성 - index 생략" %}

```javascript
PUT my_index
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1
  }
}

```

{% endcode %}

&#x20; **number\_of\_shards** 설정은 인덱스를 처음 생성할 때 한번 지정하면 바꿀 수 없습니다. 샤드 수를 바꾸려면 새로 인덱스를 정의하고 기존 인덱스의 데이터를 재색인 해야 합니다. **shrink API**또는 **split API**를 이용해서 샤드 수를 변경하는 방법이 존재하기는 하지만 인덱스를 **close** 해야 하고, 파일 재배치를 하는 작업을 하는 복잡한 과정이기에 기본적으로 샤드 수의 변경은 불가능하다 라고 인식하고 사용할 하는 것이 좋습니다.

&#x20; **number\_of\_replicas** 설정은 다이나믹하게 변경이 가능합니다. 이미 선언된 my\_index의 복제본 카피 개수를 1에서 2로 변경하려면 인덱스명 뒤에 \_settings API로 접근해서 변경할 설정만 입력해서 변경이 가능합니다.

{% code title="my\_index 인덱스의 number\_of\_replicas 값 변경" %}

```javascript
PUT my_index/_settings
{
  "number_of_replicas": 2
}
```

{% endcode %}

### refresh\_interval

&#x20; 자주 사용되는 설정 중에 **refresh\_interval** 이 있습니다. Elasticsearch 에서 세그먼트가 만들어지는 리프레시 타임을 설정하는 값인데 기본은 1초(1s) 입니다. **refresh\_interval** 역시 number\_of\_replicas 마찬가지로 설정 변경이 가능한 다이나믹 설정이며 똑같이 **settings** 의 **index** 아래에 설정합니다. 다음은 처음 인덱스를 정의할 때 refresh\_interval을 30초로 설정하는 명령입니다.

{% code title="refresh\_interval 을 30초로 my\_index 생성" %}

```javascript
PUT my_index
{
  "settings": {
    "refresh_interval": "30s"
  }
}
```

{% endcode %}

### analyzer, tokenizer, filter

&#x20; 앞 장에서 살펴보았던 애널라이저, 토크나이저, 토큰 필터 역시 setting 내부에 정의합니다. 정의하는 기본 구조는 다음과 같습니다.

{% code title="refresh\_interval 을 30초로 my\_index 생성" %}

```javascript
PUT my_index
{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_analyzer": {
          "type": "custom",
          "char_flter": [ "...", "..." ... ]
          "tokenizer": "...",
          "filter": [ "...", "..." ... ]
        }
      },
      "char_filter":{
        "my_char_filter":{
          "type": "…"
          ... 
        }
      }
      "tokenizer": {
        "my_tokenizer":{
          "type": "…"
          ...
        }
      },
      "filter": {
        "my_token_filter": {
          "type": "…"
          ...
        }
      }
    }
  }
}
```

{% endcode %}

&#x20; `"analysis": { }` 내부에 `"analyzer": { }`, `"char_filter":{ }`, `"tokenizer": { }`, `"filter": { }` 를 입력하고 각자의 내부에서 임의의 이름을 주어 각 기능들을 정의합니다. 각 내부에 하나 이상을 생성할 수도 있으며 애널라이저에서는 사용자가 정의한 토크나이저, 토큰 필터를 사용하거나 Elasticsearch 안에 미리 정의되어 있는 것들의 사용이 가능합니다.

&#x20; `"analysis": { }` 내용은 한번 생성 후 변경은 불가능합니다. 이미 만들어진 인덱스에 애널라이저나 토크나이저 등을 추가하거나 사전을 변경하려면 인덱스를 먼저 \_close 한 후에 추가하고 다시 \_open 해서 적용할 수 있습니다. 애널라이저 설정에 대한 상세 내용들은 앞의 [6.3 애널라이저](/06-text-analysis/6.3-analyzer-1) 장에 설명이 되어 있으니 필요하시면 다시 돌아가 복습 해 보시기 바랍니다.

&#x20; 이 외에도 settings 에 설정 가능한 정보가 많이 있습니다. 나중에 사용 하시면서 공식 도큐먼트를 계속 참고 하시기 바랍니다.


# 7.2 매핑 - Mappings

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

### 동적(Dynamic) 매핑

&#x20; Elasticsearch 를 활용하면서 가장 손이 많이 가는 작업이 매핑 설정입니다. Elasticsearch 는 동적 매핑을 지원하기 때문에 미리 정의하지 않아도 인덱스에 도큐먼트를 새로 추가하면 자동으로 매핑이 생성됩니다. 인덱스가 없는 상태에서 다음의 도큐먼트를 **books** 인덱스에 입력 해 보겠습니다.

{% code title="books 인덱스가 없는 상태에서 도큐먼트 입력" %}

```javascript
PUT books/_doc/1
{
  "title": "Romeo and Juliet",
  "author": "William Shakespeare",
  "category": "Tragedies",
  "publish_date": "1562-12-01T00:00:00",
  "pages": 125
}
```

{% endcode %}

&#x20; books 인덱스의 매핑을 확인 해 보면 각 필드의 매핑이 자동으로 생성된 것을 확인할 수 있습니다.

{% tabs %}
{% tab title="request" %}
{% code title="books 인덱스의 매핑 확인" %}

```javascript
GET books/_mapping
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="books 인덱스의 매핑 확인 결과" %}

```javascript
{
  "books" : {
    "mappings" : {
      "properties" : {
        "author" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "category" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "pages" : {
          "type" : "long"
        },
        "publish_date" : {
          "type" : "date"
        },
        "title" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        }
      }
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 인덱스의 매핑에서 필드들은 **mappings** 아래 **properties** 항목의 아래에 지정됩니다. 위 예제에서 보면 데이터 형식에 맞게 **title**, **author**, **category** 필드들은 **text**와 **keyword**타입으로, **pages** 필드는 **long** 타입으로, **publish\_date** 필드는 **date** 타입으로 자동 지정 된 것을 확인할 수 있습니다.

&#x20; Elasticsearch 의 매핑이 동적으로 생성 될 때는 필드의 값을 보고 타입을 예상하는데, 항상 그 필드가 포함될 수 있는 가장 넓은 범위 형태의 데이터 타입을 선택합니다. pages 필드의 값은 125로 값이 작지만 자연수를 저장하는 데이터 타입 중 가장 큰 **long** 으로 지정이 됩니다. publish\_date 필드는 값이 **"1562-12-01T00:00:00"** 로 JSON 도큐먼트에서 사용하는 [ISO8601 표준 날짜 형식](https://www.iso.org/iso-8601-date-and-time-format.html)의 데이터를 준수하였기 때문에 **date** 타입으로 인식이 되었습니다. 하지만 날짜가 **"1 Dec 1562 00:00:00"** 같이 다른 포맷으로 입력이 되면 보통은 **text** 타입으로 인식이 됩니다.

### 매핑 정의

&#x20; 데이터가 입력되어 자동으로 매핑이 생성되기 전에 미리 먼저 인덱스의 매핑을 정의 해 놓으면 정의 해 놓은 매핑에 맞추어 데이터가 입력됩니다. 매핑은 다음과 같이 선언합니다.

{% code title="인덱스의 매핑 정의" %}

```javascript
PUT <인덱스명>
{
  "mappings": {
    "properties": {
      "<필드명>":{
        "type": "<필드 타입>"
        … <필드 설정>
      }
      …
    }
  }
}
```

{% endcode %}

&#x20; 이미 만들어진 매핑에 필드를 추가하는것은 가능합니다. 하지만 이미 만들어진 필드를 삭제하거나 필드의 타입 및 설정값을 변경하는 것은 불가능합니다. 필드의 변경이 필요한 경우 인덱스를 새로 정의하고 기존 인덱스의 값을 새 인덱스에 모두 재색인 해야 합니다. 이미 생성된 인덱스에 새로운 필드를 추가 할 때는 다음과 같이 합니다.

{% code title="기존 매핑에 필드 추가" %}

```javascript
PUT <인덱스명>/_mapping
{
  "properties": {
    "<추가할 필드명>": { 
      "type": "<필드 타입>"
      … <필드 설정>
    }
  }
}
```

{% endcode %}

{% hint style="danger" %}
이 때 추가할 필드명이 기존 필드와 중복되는 이름이면 오류가 발생합니다.
{% endhint %}

&#x20; 필드 추가는 최상위 필드와 object 타입의 내부 필드, 그리고 다중 필드(multi-field) 역시 추가가 가능합니다.&#x20;

&#x20; 인덱스에 데이터가 입력될 때 기존 매핑에 정의되지 않은 필드가 도큐먼트에 있으면 필드가 자동으로 추가됩니다. **books** 인덱스에서 **page** 필드는 **byte**, **title** 필드는 **text**, **category** 필드는 **keyword** 로 선언하고 위의 첫 예제와 동일한 도큐먼트를 입력한 뒤 필드 내용을 확인해 보겠습니다.

{% code title="books 인덱스 매핑에 category, pages, title 필드 정의" %}

```javascript
PUT books
{
  "mappings": {
    "properties": {
      "category": {
        "type": "keyword"
      },
      "pages": {
        "type": "byte"
      },
      "title": {
        "type": "text"
      }
    }
  }
}
```

{% endcode %}

{% code title="books 인덱스에 도큐먼트 입력" %}

```javascript
PUT books/_doc/1
{
  "title": "Romeo and Juliet",
  "author": "William Shakespeare",
  "category": "Tragedies",
  "publish_date": "1562-12-01T00:00:00",
  "pages": 125
}
```

{% endcode %}

{% tabs %}
{% tab title="request" %}
{% code title="books 인덱스의 매핑 확인" %}

```javascript
GET books/_mapping
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="books 인덱스의 매핑 확인 결과" %}

```javascript
{
  "books" : {
    "mappings" : {
      "properties" : {
        "author" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "category" : {
          "type" : "keyword"
        },
        "pages" : {
          "type" : "byte"
        },
        "publish_date" : {
          "type" : "date"
        },
        "title" : {
          "type" : "text"
        }
      }
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 도큐먼트 입력 후 미리 정의 해 둔 **title**, **pages**, **category** 필드들은 선언된 타입 대로 유지가 되었고, **publish\_date**, **author** 필드는 디폴트 형식대로 정의되어 추가 된 것을 확인할 수 있습니다.

이제 Elasticsearch 필드에 설정 가능한 타입들을 살펴보겠습니다. 일반적으로 자바 언어 레벨에서 지원하는 **기본 타입**들과 Elasticsearch 또는 루씬 레벨에서 추상화된 **확장 타입**들이 있습니다.


# 7.2.1 문자열 - text, keyword

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Elasticsearch 에서 선언이 가능한 문자열 타입에는 **text**, **keyword** 두 가지가 있습니다. 2.x 버전 이전에 문자열은 **string** 이라는 하나의 타입만 있었고 텍스트 분석 여부, 즉 애널라이저 적용을 할 것인지 아닌지를 구분하는 설정이 있었습니다. 5.0 버전 부터는 텍스트 분석의 적용 여부를 **text** 타입과 **keyword** 타입으로 구분을 합니다. 인덱스를 생성할 때 매핑에 필드를 미리 정의하지 않으면 동적 문자열 필드가 생성 될 때 **text** 필드와 **keyword** 필드가 다중 필드로 같이 생성됩니다.

### text

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

* `"analyzer" : "<애널라이저명>"` - 색인에 사용할 애널라이저를 입력하며 디폴트로는 standard 애널라이저를 사용합니다. 토크나이저, 토큰필터들을 따로 지정할수가 없으며 필요하다면 사용자 정의 애널라이저를 settings에 정의 해 두고 사용합니다.
* `"search_analyzer" : "<애널라이저명>"` - 기본적으로 text 필드는 match 쿼리로 검색을 할 때 색인에 사용한 동일한 애널라이저로 검색 쿼리를 분석합니다. **search\_analyzer** 를 지정하면 검색시에는 색인에 사용한 애널라이저가 아닌 다른 애널라이저를 사용합니다. 보통 **NGram** 방식으로 색인을 했을 때는 지정 해 주는 것이 바람직합니다.
* `"index" : <true | false>` - 디폴트는 **true** 입니다. false로 설정하면 해당 필드는 역 색인을 만들지 않아 검색이 불가능하게 됩니다.
* `"boost" : <숫자 값>` - 디폴트는 1 입니다. 값이 1 보다 높으면 풀텍스트 검색 시 해당 필드 스코어 점수에 가중치를 부여합니다. 1보다 낮은 값을 입력하면 가중치가 내려갑니다.
* `"fielddata" : <true | false>` - 디폴트는 false 입니다. true로 설정하면 해당 필드의 색인된 텀 들을 가지고 **집계(aggregation)** 또는 **정렬(sorting)**&#xC774; 가능합니다. 이 설정은 다이나믹 설정으로 이미 정의된 매핑에 true 또는 false로 다시 적용하는 것이 가능합니다.

{% hint style="danger" %}
`"fielddata": true` 설정이 되면 쿼리에 메모리 사용량이 많아지기 때문에 일반적으로는 권장하지 않는 옵션입니다. 그리고 모든 텀 들은 애널라이저가 적용되어 처리된 형태로 집계나 정렬에 사용되기 때문에 특히 정렬 같은 경우 일반적으로 예상하는 정렬과 결과가 다르게 나오는 경우가 많습니다. 집계와 정렬은 항상 **keyword** 필드로 사용하는 것을 권장합니다.
{% endhint %}

### keyword

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

* `index`, `boost` 설정은 text 필드와 동일하게 동작합니다.
* `"doc_values" : <true | false>` - 디폴트는 true 입니다. keyword 값들은 기본적으로 집계나 정렬에 메모리를 소모하지 않기 위해 값들을 **doc\_values** 라고 하는 별도의 **열 기반 저장소(columnar store)**&#xB97C; 만들어 저장합니다. 이 값을 false로 하면 doc\_values에 값을 저장하지 않아 집계나 정렬이 불가능해집니다.
* `"ignore_above" : <자연수>` - 디폴트는 2,147,483,647 이며 다이나믹 매핑으로 생성되면 **ignore\_above: 256** 로 설정이 됩니다. 설정된 길이 이상의 문자열은 색인을 하지 않아 검색이나 집계가 불가능합니다. \_source에는 남아있기 때문에 다른 필드 값을 쿼리해서 나온 결과로 가져오는 것은 가능합니다.
* `"normalizer" : "<노멀라이저명>"` - keyword 필드는 애널라이저를 사용하지 않는 대신 **노멀라이저(normalizer)** 의 적용이 가능합니다. 노멀라이저는 애널라이저와 유사하게 settings 에서 정의하며 토크나이저는 적용할 수 없고 캐릭터 필터와 토큰 필터만 적용해서 사용이 가능합니다.

&#x20; 다음은 앞에서 살펴본 text, keyword의 옵션들을 다양하게 사용해서 **blogs** 인덱스를 정의하는 예제입니다. 각각의 옵션들이 어떤 필드에 어떻게 사용이 되었고, 명시된 각 필드들이 어떻게 동작을 할지 한번 짐작 해 보시기 바랍니다.

{% code title="blogs 인덱스 선언하면서 각 필드의 text, keyword 매핑 설정" %}

```javascript
PUT blogs
{
  "settings": {
    "analysis": {
      "analyzer": {
        "engram_a": {
          "tokenizer": "standard",
          "filter": [ "lowercase", "engram_f" ]
        }
      },
      "filter": {
        "engram_f": {
          "type": "edge_ngram",
          "min_gram": 2,
          "max_gram": 5
        }
      },
      "normalizer": {
        "norm_low": {
          "type": "custom",
          "filter": [ "lowercase", "asciifolding" ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "boost": 2,
        "fields": {
          "keyword": {
            "type": "keyword",
            "normalizer": "norm_low"
          }
        }
      },
      "author": {
        "type": "text",
        "analyzer": "engram_a",
        "search_analyzer": "standard",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "synopsis": {
        "type": "text",
        "fielddata": true
      },
      "category": {
        "type": "keyword"
      },
      "content": {
        "type": "text",
        "index": false
      }
    }
  }
}
```

{% endcode %}

&#x20; 지금까지 text와 keyword의 주요 설정에 대해 살펴보았습니다. 이 외에도 추가적으로 지정 가능한 다른 설정들이 있으니 공식 도큐먼트에서 확인 해 보시기 바랍니다.


# 7.2.2 숫자 - long, double ...

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Elasticsearch는 자바에서 기본으로 사용되는 숫자 타입들을 지원합니다. 그리고 **half\_float**, **scaled\_float** 과 같이 Elasticsearch에서만 사용되는 타입들도 있습니다.

* **`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) 같이 소수점 자리가 고정된 값을 표시할 때 유용합니다.

&#x20; 모든 숫자 필드들에 공통적으로 설정 가능한 옵션들은 다음과 같은 것들이 있습니다.

* `"index"`, `"doc_values"`, `"boost"` 옵션들은 text, keyword 필드의 옵션들과 동일합니다.
* `"coerce": <true | false>`  - 디폴트는 **true** 입니다. 숫자 필드들은 기본적으로 숫자로 이해될 수 있는 값들은 숫자로 변경해서 저장합니다. 예를 들어 integer 필드에 `4`, `"4"`, `4.5` 등을 입력하면 모두 자연수 4로 자동으로 변환되어 저장됩니다. false 로 설정하면 정확한 타입으로 입력되지 않으면 오류가 발생합니다.
* `"null_value" : <숫자값>`  - 필드값이 입력되지 않거나 null 인 경우 해당 필드의 디폴트 값을 지정합니다.
* `"ignore_malformed" : <true | false>`  - 디폴트는 **false** 입니다. 기본적으로 숫자 필드에 숫자가 아닌 문자나 불린 값이 들어오면 Elasticsearch는 오류를 리턴합니다. true로 설정하게 되면 숫자가 아닌 값이 들어와도 도큐먼트를 정상적으로 저장합니다. 하지만 해당 필드의 값은 \_source 에만 저장되고 검색이나 집계에는 무시됩니다.

&#x20; 다음은 **scaled\_float** 타입에서만 사용되는 옵션입니다.

* `"scaling_factor" : <10의 배수>`  - **scaled\_float** 를 사용하려면 필수로 지정해야 하는 옵션입니다. 소수점 몇 자리까지 저장할지를 지정합니다. 12.3456 이라는 값을 저장하는 경우 scaling\_factor: 10 으로 설정했으면 실제로는 12.3 이 저장됩니다. scaling\_factor : 100 으로 설정했으면 12.34 가 저장됩니다.

{% hint style="danger" %}
`"coerce": true` 로 인해 **"4.5"** 가 integer 필드에 정상적으로 저장 되어도 **\_source** 의 값은 그대로 **"4.5"** 입니다. 하지만 검색 또는 집계는 **4**로 적용됩니다. null\_value 옵션도 마찬가지로 \_source 에는 해당 필드가 null 또는 존재하지 않는 것으로 표시되지만 검색, 또는 집계에는 null\_value에 해당하는 값으로 적용이 됩니다.

**명심하세요. 전처리된 데이터가 아니면 항상 \_source의 값은 변경되지 않습니다.**
{% endhint %}

&#x20; 다음과 같이 my\_number 인덱스에 number\_val 필드를 byte 로 선언 한 뒤 도큐먼트들을 색인 해 보겠습니다. `"coerce"` 의 기본 값은 true 이지만 확실하게 하기 위해 이 옵션도 같이 지정하였습니다.

{% code title="my\_number 인덱스 선언" %}

```javascript
PUT my_number
{
  "mappings": {
    "properties": {
      "number_val" : {
        "type": "byte",
        "coerce" : true
      }
    }
  }
}
```

{% endcode %}

{% tabs %}
{% tab title="request" %}
{% code title="my\_bumber 인덱스에 벌크로 도큐먼트 색인" %}

```javascript
PUT my_number/_bulk
{ "index" : { "_id": "1" }}
{ "number_val": 3 }
{ "index" : { "_id": "2" }}
{ "number_val": 4.5 }
{ "index" : { "_id": "3" }}
{ "number_val": "5.2" }
{ "index" : { "_id": "4" }}
{ "number_val": 1024 }
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="my\_bumber 인덱스에 벌크로 도큐먼트 색인 결과" %}

```javascript
{
  "took" : 46,
  "errors" : true,
  "items" : [
    {
      "index" : {
        "_index" : "my_number",
        "_type" : "_doc",
        "_id" : "1",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 2,
          "failed" : 0
        },
        "_seq_no" : 0,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "index" : {
        "_index" : "my_number",
        "_type" : "_doc",
        "_id" : "2",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 2,
          "failed" : 0
        },
        "_seq_no" : 1,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "index" : {
        "_index" : "my_number",
        "_type" : "_doc",
        "_id" : "3",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 2,
          "failed" : 0
        },
        "_seq_no" : 2,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "index" : {
        "_index" : "my_number",
        "_type" : "_doc",
        "_id" : "4",
        "status" : 400,
        "error" : {
          "type" : "mapper_parsing_exception",
          "reason" : "failed to parse field [number_val] of type [byte] in document with id '4'. Preview of field's value: '1024'",
          "caused_by" : {
            "type" : "illegal_argument_exception",
            "reason" : "Value [1024] is out of range for a byte"
          }
        }
      }
    }
  ]
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 먼저 number\_val 은 byte 이기 때문에 `"_id": "4"` 도큐먼트인 1024 값은 색인이 되지 않고 오류가 발생합니다. 이제 range 쿼리를 이용해서 3 보다 크거나 같고 4.2 보다 작은 값을 검색 해 봅니다.

{% tabs %}
{% tab title="request" %}
{% code title="3 이상, 4.2 미만인 값 검색" %}

```javascript
GET my_number/_search
{
  "query": {
    "range": {
      "number_val": {
        "gte": 3,
        "lt": 4.2
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="3 이상, 4.2 미만인 값 검색 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my_number",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "number_val" : 3
        }
      },
      {
        "_index" : "my_number",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "number_val" : 4.5
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 분명 4.2 보다 작은 값을 검색했는데 결과에 4.5 값을 가진 `"_id": "2"` 도큐먼트가 같이 검색이 되었습니다. \_source 에는 4.5 로 값이 들어가 있지만 number\_val 필드는 byte 이기 때문에 실제로는 **4.5** 가 아닌 **4** 가 저장이 되어 있습니다. 따라서 4.2 보다 작은 값으로 검색이 됩니다. 같은 이유로 \_source 에는 텍스트 "5.2" 가 있는 `"_id": "3"` 도큐먼트 역시 실제로는 자연수 **5** 가 저장이 됩니다.

{% hint style="danger" %}
조금 전 살펴 본 이유로 숫자 필드를 동적으로 생성하는 것은 매우 위험합니다. 다량의 데이터가 색인 될 때 만약에 가장 처음 들어 온 도큐먼트의 숫자 값이 4 와 같은 자연수인 경우 필드는 자동으로 **long** 타입으로 생성이 됩니다. 이후에 정수가 아닌 5.5 같은 소수점을 포함한 실수가 들어와도 오류가 발생되지 않고 정상적으로 도큐먼트가 저장됩니다. 하지만 실제로는 5.5 가 아닌 정수 5 가 저장되기 때문에 나중에 검색이나 집계에 오류가 발생하게 됩니다.
{% endhint %}


# 7.2.3 날짜 - date

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Elasticsearch 에서 날짜 타입은 [ISO8601](https://www.iso.org/iso-8601-date-and-time-format.html) 형식을 따라 입력을 합니다. 일반적으로 다음과 같은 형태로 입력된 경우 자동으로 날짜 타입으로 인식이 됩니다.

* "2019-06-12"
* "2019-06-12T17:13:40"
* "2019-06-12T17:13:40+09:00"
* "2019-06-12T17:13:40.428Z"

&#x20; 위와 같은 **ISO8601** 형식이 아니라 **"2019/06/12 12:10:30"** 와 같이 입력하면 보통은 **text**, **keyword** 로 저장됩니다. 이 외에도 **1550282065513** 와 같이 **long** 타입의 정수인 **epoch\_millis** 형태의 입력도 가능합니다. **epoch\_millis** 는 **1970-01-01 00:00:00** 부터의 시간을 밀리초 단위로 카운트 한 값입니다. 필드가 **date** 형으로 정의 된 이후에는 long 타입의 정수를 입력하면 날짜 형태로 저장이 가능합니다. "2019/06/10 12:10:30" 같은 형식으로 날짜를 저장하려면 **format** 옵션을 사용해서 형태를 지정해야 합니다.

&#x20; 다음은 날짜 타입에서 사용 가능한 옵션들입니다.

* `"doc_values"`, `"index"`, `"null_value"`, `"ignore_malformed"` 옵션들은 문자열, 숫자 필드와 기능이 동일합니다.
* `"format" : "<문자열 || 문자열 ...>"` 입력 가능한 날짜 형식을 || 로 구분해서 입력합니다.

&#x20; 다음은 **my\_date** 인덱스에서 **"2019/06/10"**, **"2019-06-10 12:10:30"** 그리고 **epoch\_millis** 형태로 입력받도록 **date\_val** 날짜 필드를 지정하는 예제입니다.

{% code title="my\_date 인덱스 선언" %}

```javascript
PUT my_date
{
  "mappings": {
    "properties": {
      "date_val": {
        "type": "date",
        "format": "yyyy-MM-dd HH:mm:ss||yyyy/MM/dd||epoch_millis"
      }
    }
  }
}
```

{% endcode %}

&#x20; long 타입의 정수형은 **epoch\_millis** 외에도 **epoch\_second** 의 사용이 가능합니다. 그 외에도 **basic\_date**, **strict\_date\_time** 등과 같이 미리 정의 된 포맷을 이용하거나, [joda.time.format](https://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html) 심볼을 사용하여 지정할 수 있습니다.&#x20;

정의된 포맷들은 Elastic 홈페이지의 공식 도큐먼트 [https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html#built-in-date-formats)\
페이지에서 볼 수 있으며, joda 심볼 기호들은 다음과 같습니다.

| 심볼   | 의미                 | 예) 2019-09-12T17:13:07.428+09:00 |
| ---- | ------------------ | -------------------------------- |
| yyyy | 년도                 | 2019                             |
| MM   | 월 - 숫자             | 09                               |
| MMM  | 월 - 문자 (3자리)       | Sep                              |
| MMMM | 월 - 문자 (전체)        | September                        |
| dd   | 일                  | 12                               |
| a    | 오전 / 오후            | PM                               |
| HH   | 시각 (0\~23)         | 17                               |
| kk   | 시각 (01\~24)        | 17                               |
| hh   | 시각 (01\~12)        | 05                               |
| h    | 시각 (1\~12)         | 5                                |
| mm   | 분 (00\~59)         | 13                               |
| m    | 분 (0\~59)          | 13                               |
| ss   | 초 (00\~59)         | 07                               |
| s    | 초 (0\~59)          | 7                                |
| SSS  | 밀리초                | 428                              |
| Z    | 타임존                | +0900 / +09:00                   |
| e    | 요일 (숫자 1:월 \~ 7:일) | 4                                |
| E    | 요일 (텍스트)           | Thu                              |

&#x20; 날짜 필드는 입력된 값들을 실제로 내부에서는 모두 **long** 형태의 **epoch\_millis** 로 저장합니다. 또한 매핑의 **format** 형식만 지정 해 놓으면 지정된 어떤 형식으로도 색인 및 쿼리가 가능합니다. 다시 말해 \_source 의 날짜 는 **"2019-09-12"** 형식으로 입력 되었어도 **"2019/09/12"** 형식으로 range 쿼리를 해도 정상적으로 동작합니다. [range 쿼리](/05-search/5.6-range)는 5장 검색에서 확인 하시기 바랍니다.

&#x20; 다음은 앞에서 선언한 **my\_date** 인덱스에 `"date_val": "2019-09-12 15:01:23"` 인 도큐먼트를 입력하고 "2019/09/10" 보다 크고 "2019-09-13 12:00:00" 의 epoch\_millis 값인 1568332800000 보다 작은 값을 검색하는 쿼리입니다.

{% code title=""yyyy-MM-dd HH:mm:ss" 형식으로 날짜값 입력" %}

```javascript
PUT my_date/_doc/1
{
  "date_val": "2019-09-12 15:01:23"
}
```

{% endcode %}

{% tabs %}
{% tab title="request" %}
{% code title=""yyyy/MM/dd", epoch\_millis 형식으로 날짜 검색" %}

```javascript
GET my_date/_search
{
  "query": {
    "range": {
      "date_val": {
        "gt": "2019/09/10",
        "lt": 1568332800000
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title=""yyyy/MM/dd", epoch\_millis 형식으로 날짜 검색 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my_date",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "date_val" : "2019-09-12 15:01:23"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 입력된 포맷과 검색 쿼리 포맷이 달라도 정상적으로 검색되는 것을 확인할 수 있습니다.


# 7.2.4 불리언 - boolean

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 불리언은 **true** 와 **false** 두가지 값을 갖는 필드 타입입니다. 선언은 `"type": "boolean"` 로 합니다. `"true"` 와 같이 문자열로 입력이 되어도 `true` 로 해석이 되어 저장됩니다. 불리언 필드를 사용 할 때는 일반적으로 term 쿼리를 이용해서 검색을 합니다.

&#x20; 다음은 불리언 필드에서 사용 가능한 옵션들입니다.

* `"doc_values"`, `"index"` 옵션들은 문자열, 숫자 필드와 기능이 동일합니다.
* `"null_value" : <true | false>`  - 필드가 존재하지 않거나 값이 **null** 일 때 디폴트 값을 지정합니다. 지정하지 않으면 불리언 필드가 없거나 값이 null인 경우 존재하지 않는 것으로 처리되어 true / false 모두 쿼리나 집계에 나타나지 않습니다.


# 7.2.5 Object 와 Nested

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

### Object

&#x20; JSON 에서는 한 필드 안에 하위 필드를 넣는 **object**, 즉 **객체** 타입의 값을 사용할 수 있습니다. 보통은 한 요소가 여러 하위 정보를 가지고 있는 경우 object 타입 형태로 사용합니다. 다음은 **movie** 인덱스에 하위 필드 **"name"**, **"age"**, **"side"** 를 가진 **object** 타입 **"characters"** 필드의 예제입니다.

{% code title="object 타입 characters 필드를 가진 도큐먼트" %}

```javascript
PUT movie/_doc/1
{
  "characters": {
    "name": "Iron Man",
    "age": 46,
    "side": "superhero"
  }
}
```

{% endcode %}

&#x20; object 필드를 선언 할 때는 다음과 같이 `"properties"` 를 입력하고 그 아래에 하위 필드 이름과 타입을 지정합니다.

{% code title="매핑에 object 타입 characters 필드 선언" %}

```javascript
PUT movie
{
  "mappings": {
    "properties": {
      "characters": {
        "properties": {
          "name": {
            "type": "text"
          },
          "age": {
            "type": "byte"
          },
          "side": {
            "type": "keyword"
          }
        }
      }
    }
  }
}
```

{% endcode %}

&#x20; object 필드를 쿼리로 검색 하거나 집계를 할 때는 다음과 같이 마침표 `.` 를 이용해서 하위 필드에 접근합니다.

{% tabs %}
{% tab title="request" %}
{% code title="characters 하위의 name 필드 쿼리" %}

```javascript
GET movie/_search
{
  "query": {
    "match": {
      "characters.name": "Iron Man"
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="characters 하위의 name 필드 쿼리 결과" %}

```javascript
{
  "took" : 262,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.5753642,
    "hits" : [
      {
        "_index" : "movie",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.5753642,
        "_source" : {
          "characters" : {
            "name" : "Iron Man",
            "age" : 46,
            "side" : "superhero"
          }
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% hint style="info" %}
Elasticsearch에는 따로 배열(array) 타입의 필드를 선언하지 않습니다. 필드 타입의 값만 일치하면 다음과 같이 값을 배열로도 넣을 수 있습니다.

* { "title": "Romeo and Juliet" }
* { "title": \[ "Romeo and Juliet", "Hamlet" ] }
  {% endhint %}

&#x20; 이번에는 다음과 같이 **title** 필드 값이 각각 **"The Avengers"**, **"Avengers: Infinity War"** 이고 **characters** 필드에 object 값이 2개씩 들어있는 두 개의 도큐먼트를 입력 해 보겠습니다.

{% code title="characters 필드에 2개의 ojbect 값들을 배열로 가진 도큐먼트 2개 입력" %}

```javascript
PUT movie/_doc/2
{
  "title": "The Avengers",
  "characters": [
    {
      "name": "Iron Man",
      "side": "superhero"
    },
    {
      "name": "Loki",
      "side": "villain"
    }
  ]
}

PUT movie/_doc/3
{
  "title": "Avengers: Infinity War",
  "characters": [
    {
      "name": "Loki",
      "side": "superhero"
    },
    {
      "name": "Thanos",
      "side": "villain"
    }
  ]
}
```

{% endcode %}

&#x20; 두 개 도큐먼트 모두 characters 필드의 하위 필드 값으로 `"name": "Loki"` 가 있습니다. 그리고 한 도큐먼트는 "name": "Loki" 의 "side" 필드 값이 **"villain"** 이고 다른 도큐먼트는 **"superhero"** 입니다. 이제 characters 필드의 name 값은 "Loki" 이고 side 값은 "villain" 인 도큐먼트를 검색 해 보겠습니다.

{% tabs %}
{% tab title="request" %}
{% code title="characters 하위 필드의 name: Loki, side: villain 검색" %}

```javascript
GET movie/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "characters.name": "Loki"
          }
        },
        {
          "match": {
            "characters.side": "villain"
          }
        }
      ]
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="characters 하위 필드의 name: Loki, side: villain 검색 결과" %}

```javascript
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0611372,
    "hits" : [
      {
        "_index" : "movie",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0611372,
        "_source" : {
          "title" : "Avengers: Infinity War",
          "characters" : [
            {
              "name" : "Loki",
              "side" : "superhero"
            },
            {
              "name" : "Thanos",
              "side" : "villain"
            }
          ]
        }
      },
      {
        "_index" : "movie",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.9827781,
        "_source" : {
          "title" : "The Avengers",
          "characters" : [
            {
              "name" : "Iron Man",
              "side" : "superhero"
            },
            {
              "name" : "Loki",
              "side" : "villain"
            }
          ]
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 분명 `{"name": "Loki", "side": "villain"}` 값을 포함하고 있는 도큐먼트는 `"_id" : "2"` 인 `"title": "The Avengers"` 도큐먼트 뿐인데 `{"name": "Loki", "side": "superhero"}` 를 포함하고 있는 `"title": "Avengers: Infinity War"` 도큐먼트도 같이 검색이 되었습니다. *(심지어 스코어도 더 높습니다)*

&#x20; 얼핏 생각했을 때는 `"title": "The Avengers"` 도큐먼트만 검색이 되어야 맞는 것으로 생각이 되는데 실제 결과는 그렇지가 않습니다. 이유는 Elasticsearch는 위 예제에서 역 색인을 다음과 같은 모양으로 생성하기 때문입니다. 기억하세요. 역 색인은 필드 별로 생성됩니다.

![characters 하위 필드들의 역 색인 구조](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LpGQaUnYGC9WKlnKtQH%2F-LpGQd62hk9lYfOG1h9w%2F07-01.png?alt=media\&token=dbd7b0a6-7f46-444f-af66-a81a6aa10637)

&#x20; 역 색인에서는 object 필드의 하위 필드들은 모두 상위 필드의 이름과 함께 펼쳐져서 한 필드로 저장이 됩니다. 그렇기 때문에 `"characters.name": "Loki"`, `"characters.side": "villain"` 두 필드값들은 `"_id" : "2"` , `"_id" : "3"` 두 도큐먼트 모두를 리턴합니다.

### Nested

&#x20; 만약에 object 타입 필드에 있는 여러 개의 object 값들이 서로 다른 역 색인 구조를 갖도록 하려면 **nested** 타입으로 지정해야 합니다. nested 타입으로 지정하려면 매핑에 다음과 같이 `"type": "nested"` 를 명시합니다. 다른 부분은 object 와 동일합니다.

{% code title="매핑에 nested 타입 characters 필드 선언" %}

```javascript
PUT movie
{
  "mappings": {
    "properties": {
      "characters": {
        "type": "nested",
        "properties": {
          "name": {
            "type": "text"
          },
          "side": {
            "type": "keyword"
          }
        }
      }
    }
  }
}
```

{% endcode %}

&#x20; 입력할 데이터는 의 object 예제에서 입력 한 데이터와 동일합니다. 매핑을 위와 같이 nested 형식으로 선언하고 앞에서 했던 **title** 필드 값이 각각 **"The Avengers"**, **"Avengers: Infinity War"** 이고 characters 필드에 object 값이 2개씩 들어있는 두 개의 도큐먼트를 다시 한번 입력 해 보도록 합니다. 그 뒤 다시 characters 필드의 name 값은 **"Loki"** 이고 side 값은 **"villain"** 인 도큐먼트를 검색 해 보도록 합니다.

{% tabs %}
{% tab title="request" %}
{% code title="characters 하위 필ㄹ드의 name: Loki, side: villain 검색" %}

```javascript
GET movie/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "characters.name": "Loki"
          }
        },
        {
          "match": {
            "characters.side": "villain"
          }
        }
      ]
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="characters 하위 필ㄹ드의 name: Loki, side: villain 검색 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 검색 결과가 하나도 나타나지 않았습니다.

&#x20; nested 필드를 검색 할 때는 반드시 **nested 쿼리**를 써야 합니다. nested 쿼리 안에는 **path** 라는 옵션으로 nested로 정의된 필드를 먼저 명시하고 그 안에 다시 쿼리를 넣어서 입력합니다.

{% tabs %}
{% tab title="request" %}
{% code title="nested 쿼리로 characters 하위 필드의 name: Loki, side: villain 검색" %}

```javascript
GET movie/_search
{
  "query": {
    "nested": {
      "path": "characters",
      "query": {
        "bool": {
          "must": [
            {
              "match": {
                "characters.name": "Loki"
              }
            },
            {
              "match": {
                "characters.side": "villain"
              }
            }
          ]
        }
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="nested 쿼리로 characters 하위 필드의 name: Loki, side: villain 검색 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.4480599,
    "hits" : [
      {
        "_index" : "movie",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.4480599,
        "_source" : {
          "title" : "The Avengers",
          "characters" : [
            {
              "name" : "Iron Man",
              "side" : "superhero"
            },
            {
              "name" : "Loki",
              "side" : "villain"
            }
          ]
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 결과로 `{"name" : "Loki", "side" : "villain"}` 값을 포함하고 있는 `"_id" : "2"` 도큐먼트만 검색이 되었습니다.

&#x20; nested 쿼리로 검색하면 nested 필드의 내부에 있는 값 들을 모두 별개의 도큐먼트로 취급합니다. 앞의 예제에서 본 object 도큐먼트와 nested 도큐먼트를 그림으로 비교 해 보면 다음과 같습니다.

![](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LpGYSs0zuXyA9FUFgn2%2F-LpGYVcM11V3j4-kGl7O%2F07-02.png?alt=media\&token=d5e3f05b-9fea-404c-976f-d2d1b4ace1ab)

&#x20; object 필드 값들은 실제로 하나의 도큐먼트 안에 전부 포함되어 있습니다. 반면에 nested 필드 값들은 내부적으로 별도의 도큐먼트로 분리되어 저장되며 쿼리 결과에서 상위 도큐먼트와 합쳐져서 보여지게 됩니다.


# 7.2.6 위치 정보 - Geo

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 검색엔진을 사용하는 여러 서비스들 중에 요즘은 모바일 기기들을 이용해서 위치 정보를 표시하거나 검색하는 서비스들이 많이 있습니다. Elasticsearch는 자바와 기타 프로그래밍 언어에서 제공하는 기본 데이터 타입 외에도 여러가지의 추상화 된 데이터 타입들이 있습니다. 그 중에 이런 위치정보를 저장할 수 있는 **Geo Point** 와 **Geo Shape** 같은 타입들이 있습니다.

### Geo Point

&#x20; **Geo Point** 는 위도(**latitude**)와 경도(**longitude**) 두 개의 실수 값을 가지고 지도 위의 한 점을 나타내는 값입니다. Geo Point 필드의 값들은 다음과 같이 다양한 방법으로 입력이 가능합니다.

{% code title="object 형식으로 geo\_point 입력" %}

```javascript
PUT my_locations/_doc/1
{
  "location": {
    "lat": 41.12,
    "lon": -71.34
  }
}
```

{% endcode %}

{% code title="text 형식으로 geo\_point 입력" %}

```javascript
PUT my_index/_doc/2
{
  "location": "41.12,-71.34"
}
```

{% endcode %}

{% code title="geohash 형식으로 geo\_point 입력" %}

```javascript
PUT my_index/_doc/3
{
  "location": "drm3btev3e86"
}
```

{% endcode %}

{% code title="실수의 배열 형식으로 geo\_point 입력" %}

```javascript
PUT my_index/_doc/4
{
  "location": [
    -71.34,
    41.12
  ]
}
```

{% endcode %}

&#x20; Text 와 실수 방식은 위도와 경도의 입력 순서가 서로 반대이기 때문에 헷갈리기 쉽습니다. 그래서 보통은 `{"lat": 41.12, "lon": -71.34}` 와 같이 알아보기 편한 **object** 형식으로 입력합니다.

&#x20; **geohash** 는 전 세계 지도를 바둑판 모양의 격자로 나누어 각 칸 마다 숫자와 알파벳으로 기호를 메기고, 그 칸을 다시 나누어 다시 기호를 추가하는 방식으로 표현한 것입니다. 자릿수가 커질수록 정밀도가 높아집니다. 보통 1자리 값이면 대륙, 2자리 값이면 대한민국 영토 정도의 크기이고 4자리 값이면 대도시, 7자리 값이면 길거리 한 블록 정도의 정밀도를 나타냅니다. 아래 예시에서 `sg` 값은 북유럽 스칸디나비아 반도 부근의 크기입니다.

![geohash 로 위치 정보 표시](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LpWuejGDmkMWhS0V4-H%2F-LpWwVvQic1qG95C6bOk%2F07-03.png?alt=media\&token=923ea1be-5f13-44ee-9cf6-5de946754428)

&#x20; Geo Point 필드는 매핑에서 다음과 같이 `"type": "geo_point"` 로 선언합니다.

{% code title="object 형식으로 geo\_point 입력" %}

```javascript
PUT my_geo
{
  "mappings": {
    "properties": {
      "location": {
        "type": "geo_point"
      }
    }
  }
}
```

{% endcode %}

&#x20; Geo Point 필드의 경우는 반드시 데이터를 입력하기 전에 인덱스 매핑을 정의 해 주어야 합니다. 매핑을 정의하지 않고 `{ "location": { "lat" : 41.12, "lon": -71.34 } }` 와 같은 값을 입력하면 다이나믹 매핑으로 필드가 자동 생성될 때 geo\_point 타입의 필드가 생기는 것이 아니라 다음과 같이 **float** 타입의 **lat, lon** 두개의 하위 필드가 생깁니다.

{% tabs %}
{% tab title="request" %}
{% code title="도큐먼트 입력으로 my\_geo 인덱스 동적 생성 후 매핑 확인" %}

```javascript
PUT my_geo/_doc/1
{
  "location": {
    "lat": 41.12,
    "lon": -71.34
  }
}

GET my_geo/_mapping
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="도큐먼트 입력으로 my\_geo 인덱스 동적 생성 후 매핑 확인 결과" %}

```javascript
{
  "my_geo" : {
    "mappings" : {
      "properties" : {
        "location" : {
          "properties" : {
            "lat" : {
              "type" : "float"
            },
            "lon" : {
              "type" : "float"
            }
          }
        }
      }
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

### geo\_bounding\_box 쿼리

&#x20; Elasticsearch에는 위치정보를 검색할 수 있는 다양한 쿼리들이 있습니다. **geo\_point** 값의 검색에 주로 사용 되는 것은 **geo\_bounding\_box** 쿼리와 **geo\_distance** 쿼리 입니다. 예제 위해 먼저 다음 도큐먼트들을 my\_geo 인덱스에 벌크로 입력하겠습니다. location 필드의 타입을 `"type": "geo_point"` 로 매핑을 먼저 설정해야 하는 것을 잊지 마세요.

{% code title="my\_geo 인덱스에 예제 데이터 입력" %}

```javascript
PUT my_geo/_bulk
{"index":{"_id":"1"}}
{"station":"강남","location":{"lon":127.027926,"lat":37.497175},"line":"2호선"}
{"index":{"_id":"2"}}
{"station":"종로3가","location":{"lon":126.991806,"lat":37.571607},"line":"3호선"}
{"index":{"_id":"3"}}
{"station":"여의도","location":{"lon":126.924191,"lat":37.521624},"line":"5호선"}
{"index":{"_id":"4"}}
{"station":"서울역","location":{"lon":126.972559,"lat":37.554648},"line":"1호선"}
```

{% endcode %}

&#x20; **geo\_bounding\_box** 쿼리는 **top\_left** 와 **bottom\_right** 두 개의 옵션에 각각 위치점을 입력하고 이 점들을 토대로 그린 네모 칸 안에 위치하는 도큐먼트들을 불러옵니다. 다음은 **{"lat": 37.4899, "lon": 127.0388}**, **{"lat": 37.5779, "lon": 126.9617}** 두 점을 기준으로 하는 네모 영역 안에 있는 도큐먼트들을 가져오는 예제입니다.

{% tabs %}
{% tab title="request" %}
{% code title="geo\_bounding\_box 쿼리로 네모 영역 안에 있는 도큐먼트 쿼리" %}

```javascript
GET my_geo/_search
{
  "query": {
    "geo_bounding_box": {
      "location": {
        "bottom_right": {
          "lat": 37.4899,
          "lon": 127.0388
        },
        "top_left": {
          "lat": 37.5779,
          "lon": 126.9617
        }
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="geo\_bounding\_box 쿼리로 네모 영역 안에 있는 도큐먼트 쿼리 결과" %}

```javascript
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my_geo",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "station" : "강남",
          "location" : {
            "lon" : 127.027926,
            "lat" : 37.497175
          },
          "line" : "2호선"
        }
      },
      {
        "_index" : "my_geo",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "station" : "종로3가",
          "location" : {
            "lon" : 126.991806,
            "lat" : 37.571607
          },
          "line" : "3호선"
        }
      },
      {
        "_index" : "my_geo",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 1.0,
        "_source" : {
          "station" : "서울역",
          "location" : {
            "lon" : 126.972559,
            "lat" : 37.554648
          },
          "line" : "1호선"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; `"station" : "강남"`, `"station" : "종로3가"`, `"station" : "서울역"` 총 3개의 결과가 리턴 되었습니다. 위 쿼리 내용을 지도에 표현 해 보면 다음과 같습니다.

![geo\_bounding\_box 쿼리 결과](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-Lp_ctCIv2kgHnsjYYwB%2F-Lp_eDIWNGl2jDyirYva%2F07-04.png?alt=media\&token=da463dfe-5386-458f-bb06-530a4786f23b)

### geo\_distance 쿼리

&#x20; **geo\_distance** 쿼리는 하나의 위치점을 찍고 **distance** 옵션을 이용해서 입력한 반경의 원 안에 있는 도큐먼트들을 불러옵니다. 다음은 **{"lat": 37.5358, "lon": 126.9559}** 기준으로 **반경 5킬로미터** 안에 있는 도큐먼트들을 불러오는 예제입니다.

{% tabs %}
{% tab title="request" %}
{% code title="geo\_bounding\_box 쿼리로 네모 영역 안에 있는 도큐먼트 쿼리" %}

```javascript
GET my_geo/_search
{
  "query": {
    "geo_distance": {
      "distance": "5km",
      "location": {
        "lat": 37.5358,
        "lon": 126.9559
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="geo\_bounding\_box 쿼리로 네모 영역 안에 있는 도큐먼트 쿼리 결과" %}

```javascript
{
  "took" : 71,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my_geo",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "station" : "여의도",
          "location" : {
            "lon" : 126.924191,
            "lat" : 37.521624
          },
          "line" : "5호선"
        }
      },
      {
        "_index" : "my_geo",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 1.0,
        "_source" : {
          "station" : "서울역",
          "location" : {
            "lon" : 126.972559,
            "lat" : 37.554648
          },
          "line" : "1호선"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; `"station" : "여의도"`, `"station" : "서울역"` 총 2개의 결과가 리턴되었습니다. **distance** 값을 10km, 20km 등으로 변경해서 다시 쿼리를 해 보면 종로3가, 강남이 결과에 나타나는 것도 확인할 수 있습니다. 위 쿼리를 지도에 표시 해 보면 다음과 같습니다.

![geo\_distance 쿼리 결과](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-Lp_ctCIv2kgHnsjYYwB%2F-Lp_fEtV5seasGrPkPYg%2F07-05.png?alt=media\&token=a0e1fb12-6e2e-4929-a600-6d1e1fb1cdb8)

### Geo Shape

&#x20; 앞에서 살펴본 Geo Point 는 위도 경도 두개의 값을 가진 1차원 데이터 "점" 입니다. Elasticsearch 에서 사용 가능한 또 다른 위치정보 타입인 **Geo Shape** 은 선, 면 등의 2차원 값을 저장하고 쿼리할 수 있습니다. Geo Shape 필드는 `"type": "geo_shape"` 으로 선언합니다.

{% code title="geo\_shape 필드 선언" %}

```javascript
PUT my_shape
{
  "mappings": {
    "properties": {
      "location": {
        "type": "geo_shape"
      }
    }
  }
}
```

{% endcode %}

&#x20; 도큐먼트에도 점, 선, 다중점, 다중선, 다각형 등을 `"type"` 값에 각각 다음과 같이 지정하고 `"coordinates"` 값에 위치 정보를 **\[ -71.34, 41.12 ]** 같이 **\[경도, 위도]** 의 순서로 배열 형식으로 입력합니다. 위치정보 순서가 반대로 되면 엉뚱한 값이 되기 때문에 주의해야 합니다.

* `"type": "point"` - 단일 점 입니다. 보통은 geo\_point 와 같은 용도로 사용됩니다.

{% code title=""type": "point" 형태의 geo\_shape 값 입력" %}

```javascript
PUT my_shape/_doc/1
{
  "location": {
    "type": "point",
    "coordinates": [
      127.027926,
      37.497175
    ]
  }
}
```

{% endcode %}

!["type": "point" 으로 선언한 값 - 점](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-Lp_imwiJ2HU6autwazE%2F-Lp_kI2X313lyKgC85Zc%2F07-06.png?alt=media\&token=8c49701e-28c7-4faf-9d38-d877d1ea5acd)

* `"type": "multipoint"` - 여러 점을 하나의 값으로 저장합니다. 점들을 배열로 입력합니다.

{% code title=""type": "multipoint" 형태의 geo\_shape 값 입력" %}

```javascript
PUT my_shape/_doc/2
{
  "location": {
    "type": "multipoint",
    "coordinates": [
      [ 127.027926, 37.497175 ],
      [ 126.991806, 37.571607 ],
      [ 126.924191, 37.521624 ],
      [ 126.972559, 37.554648 ]
    ]
  }
}
```

{% endcode %}

!["type": "multipoint" 로 선언한 값 - 다중 점](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-Lp_imwiJ2HU6autwazE%2F-Lp_l0Er5STYKczA53bp%2F07-07.png?alt=media\&token=f76a8900-71b2-4dbc-bf85-b1830d363de8)

* `"type": "linestring"` - 점 2개 값를 배열로 입력하여 두 점을 잇는 **직선**을 저장합니다. 비행 경로 등을 저장할때 유용합니다.

{% code title=""type": "linestring" 형태의 geo\_shape 값 입력" %}

```javascript
PUT my_shape/_doc/3
{
  "location": {
    "type": "linestring",
    "coordinates": [
      [ 127.027926, 37.497175 ],
      [ 126.991806, 37.571607 ]
    ]
  }
}
```

{% endcode %}

!["type": "linestring" 으로 선언 한 값 - 직선](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-Lp_imwiJ2HU6autwazE%2F-Lp_loekoMelbitAwrpO%2F07-08.png?alt=media\&token=a6ae9dc6-d6fb-4b37-a11f-6e861b0cf478)

* `"type": "multilinestring"` - 여러개의 직선을 배열로 입력하여 저장합니다.

{% code title=""type": "multilinestring" 형태의 geo\_shape 값 입력" %}

```javascript
PUT my_shape/_doc/4
{
  "location": {
    "type": "multilinestring",
    "coordinates": [
      [
        [ 127.027926, 37.497175 ],
        [ 126.991806, 37.571607 ]
      ],
      [
        [ 126.924191, 37.521624 ],
        [ 126.972559, 37.554648 ]
      ]
    ]
  }
}
```

{% endcode %}

!["type": "multilinestring" 으로 선언 한 값 - 다중 직선](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-Lp_md1mocgZ0eLD7r7h%2F-Lp_nDwZLL_XE15iMS9h%2F07-09.png?alt=media\&token=d6746821-31ee-4a79-aaf3-78ad14d34962)

* `"type": "polygon"` - 다각형을 저장합니다. 내부에 배열을 추가하고 점들을 배열로 입력하며 순서대로 이어집니다. **배열 마지막에는 반드시 처음과 같은 점이 입력되어야 합니다.** 영토 정보 등을 저장할때 유용합니다.

{% code title=""type": "polygon" 형태의 geo\_shape 값 입력" %}

```javascript
PUT my_shape/_doc/5
{
  "location": {
    "type": "polygon",
    "coordinates": [
      [
        [ 127.027926, 37.497175 ],
        [ 126.991806, 37.571607 ],
        [ 126.924191, 37.521624 ],
        [ 126.972559, 37.554648 ],
        [ 127.027926, 37.497175 ]
      ]
    ]
  }
}
```

{% endcode %}

!["type": "polygon" 으로 선언한 값 - 다각형](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-Lp_md1mocgZ0eLD7r7h%2F-Lp_oFnTQPmj0r27dsWp%2F07-10.png?alt=media\&token=39d901c2-7150-415b-ac2b-9c65593fecce)

* `"type": "multipolygon"` - 여러 개의 다각형을 배열로 저장합니다. 미국의 알래스카, 하와이, 본토 등과 같이 분리된 영토를 같이 저장하는데 유용합니다.

{% code title=""type": "multipolygon" 형태의 geo\_shape 값 입력" %}

```javascript
PUT my_shape/_doc/6
{
  "location": {
    "type": "multipolygon",
    "coordinates": [
      [
        [
          [ 127.027926, 37.497175 ],
          [ 126.991806, 37.571607 ],
          [ 126.924191, 37.521624 ],
          [ 127.004943, 37.504810 ],
          [ 127.027926, 37.497175 ]
        ]
      ],
      [
        [
          [ 126.936893, 37.555134 ],
          [ 126.967894, 37.529170 ],
          [ 126.924191, 37.521624 ],
          [ 126.936893, 37.555134 ]
        ]
      ]
    ]
  }
}
```

{% endcode %}

!["type": "multipolygon" 으로 선언한 값 - 다중 다각형](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-Lp_md1mocgZ0eLD7r7h%2F-Lp_p_kPsgW6FPMiTO1u%2F07-11.png?alt=media\&token=2d271918-d6c1-4a6e-8f8f-42498fdb4894)

* `"type": "envelope"` - 바른 각도의 직사각형 영역을 저장할때는 **polygon** 으로 4개의 점을 입력하여 지정할 수도 있지만, 대신 **envelope** 을 사용하면 좌측 **상단(upper left)** 와 **우측 하단(lower right)** 점 두개만 이용해서 그리는 것도 가능합니다.

{% code title=""type": "envelope" 형태의 geo\_shape 값 입력" %}

```javascript
PUT my_shape/_doc/7
{
  "location": {
    "type": "envelope",
    "coordinates": [
      [ 126.936893, 37.555134 ],
      [ 127.004943, 37.50481 ]
    ]
  }
}
```

{% endcode %}

!["type": "envelope" 으로 선언한 값 - 직사각형](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-Lp_rFSrDaXGUfVaxCr5%2F-Lp_tFtMHwQ_ntRZVKcb%2F07-12.png?alt=media\&token=3f4f02b0-50cc-498a-935e-64756322c1ba)

{% hint style="warning" %}
6.5 이전 버전 까지는 한 점의 coordinates 와 `"radius": "5km"` 같은 반경을 지정하는 값을 이용해서 `"type": "circle"` 인 원 형태의 데이터도 사용이 가능했습니다. 버전6.6 부터는 geo\_shape 데이터의 저장 방식이 변경되면서 6.6 버전 이후 부터 필자가 글을 쓰고 있는 현재 7.4 버전에서의 원 형태의 데이터는 지원하지 않고 있습니다. 이후 다시 구현 될 가능성은 있습니다.
{% endhint %}

### geo\_shape 쿼리

&#x20; Geo Shape 타입의 값들을 검색하려면 **geo\_shape** 쿼리를 사용해야 합니다. 입력해야하는 옵션으로는 `"shape": { }` 에 검색할 영역의 `"type"` 과 `"coordinates"` 값을 입력하고, `"relation"` 에 검색할 영역과 검색되는 도큐먼트가 겹치거나 포함되는 관계 조건 값을 입력합니다. relation 옵션에 입력 가능한 값은 **intersects**, **disjoint**, **within** 3가지가 있습니다.

* `"relation": "intersects"` - 디폴트 값입니다. 쿼리 영역과 도큐먼트 값 영역이 일부라도 겹쳐지면 참 입니다.
* `"relation": "disjoint"` - 도큐먼트 값 영역이 쿼리 영역과 겹치지 않는 쿼리 영역 바깥에 있는 도큐먼트들을 가져옵니다.
* `"relation": "within"` - 도큐먼트의 값들이 모두 쿼리 영역 안에 완전히 포함 되어 있는 도큐먼트들을 가져옵니다.

&#x20; 예제 실행을 위해 my\_shape 인덱스에 다음 세 개의 **envelope** 방식의 **geo\_shape** 값을 입력하겠습니다. 먼저 **location** 필드의 매핑을 `"type": "geo_shape"` 으로 지정해야 하는 것을 명심하세요.

{% code title="geo\_shape 예제를 위한 3개의 도큐먼트 입력" %}

```javascript
PUT my_shape/_doc/1
{
  "place": "경복궁",
  "location": {
    "type": "envelope",
    "coordinates": [
      [ 126.9735, 37.5837 ],
      [ 126.9802, 37.5756 ]
    ]
  }
}

PUT my_shape/_doc/2
{
  "place": "명동",
  "location": {
    "type": "envelope",
    "coordinates": [
      [ 126.9778, 37.5656 ],
      [ 126.9884, 37.5558 ]
    ]
  }
}

PUT my_shape/_doc/3
{
  "place": "홍대",
  "location": {
    "type": "envelope",
    "coordinates": [
      [ 126.9199, 37.5583 ],
      [ 126.9347, 37.5481 ]
    ]
  }
}
```

{% endcode %}

&#x20; 이제 **geo\_shape** 쿼리를 이용해서 **"top\_left": {"lat": 37.5800, "lon": 126.9687}**, **"bottom\_right": {"lat": 37.5543, "lon": 126.9900}** 의 직사각형 (envelope) 범위 안에 있는 도큐먼트 들을 relation 별로 검색을 해 보겠습니다. 먼저 위에 입력한 도큐먼트와 이후 검색할 쿼리 범위를 지도에 표시하면 다음과 같습니다.

![입력된 예제 도큐먼트와 쿼리 범위](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-Lp_rFSrDaXGUfVaxCr5%2F-Lp_tl6TiKIscNZU53sE%2F07-13.png?alt=media\&token=b4076bf4-5f83-4f55-a2d6-9c45e7f3feaf)

&#x20; 다음은 **geo\_shape** 쿼리의 **relation** 값을 각각 **intersects**, **within**, **disjoint** 한 결과입니다.

{% tabs %}
{% tab title="request" %}
{% code title=""relation": "intersects" 으로 geo\_shape 쿼리" %}

```javascript
GET my_shape/_search
{
  "query": {
    "geo_shape": {
      "location": {
        "shape": {
          "type": "envelope",
          "coordinates": [
            [ 126.9687, 37.58 ],
            [ 126.99, 37.5543 ]
          ]
        },
        "relation": "intersects"
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title=""relation": "intersects" 으로 geo\_shape 쿼리 결과" %}

```javascript
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my_shape",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "place" : "경복궁",
          "location" : {
            "type" : "envelope",
            "coordinates" : [
              [
                126.9735,
                37.5837
              ],
              [
                126.9802,
                37.5756
              ]
            ]
          }
        }
      },
      {
        "_index" : "my_shape",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "place" : "명동",
          "location" : {
            "type" : "envelope",
            "coordinates" : [
              [
                126.9778,
                37.5656
              ],
              [
                126.9884,
                37.5558
              ]
            ]
          }
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

* `"relation": "intersects"` - 쿼리 영역에 조금이라도 걸쳐 있는 `"place" : "경복궁"` 과 `"place" : "명동"` 도큐먼트들이 결과로 나타납니다.

{% tabs %}
{% tab title="request" %}
{% code title=""relation": "within" 으로 geo\_shape 쿼리" %}

```javascript
GET my_shape/_search
{
  "query": {
    "geo_shape": {
      "location": {
        "shape": {
          "type": "envelope",
          "coordinates": [
            [ 126.9687, 37.58 ],
            [ 126.99, 37.5543 ]
          ]
        },
        "relation": "within"
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title=""relation": "within" 으로 geo\_shape 쿼리 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my_shape",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "place" : "명동",
          "location" : {
            "type" : "envelope",
            "coordinates" : [
              [
                126.9778,
                37.5656
              ],
              [
                126.9884,
                37.5558
              ]
            ]
          }
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

* `"relation": "within"` - 쿼리 영역에 완전히 포함되어 있는 `"place" : "명동"` 도큐먼트만 결과로 나타납니다.

{% tabs %}
{% tab title="request" %}
{% code title=""relation": "disjoint" 으로 geo\_shape 쿼리" %}

```javascript
GET my_shape/_search
{
  "query": {
    "geo_shape": {
      "location": {
        "shape": {
          "type": "envelope",
          "coordinates": [
            [ 126.9687, 37.58 ],
            [ 126.99, 37.5543 ]
          ]
        },
        "relation": "disjoint"
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title=""relation": "disjoint" 으로 geo\_shape 쿼리 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my_shape",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "place" : "홍대",
          "location" : {
            "type" : "envelope",
            "coordinates" : [
              [
                126.9199,
                37.5583
              ],
              [
                126.9347,
                37.5481
              ]
            ]
          }
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

* `"relation": "disjoint"` - 쿼리 영역 바깥에 있는 `"place" : "홍대"` 도큐먼트가 결과로 나타납니다. &#x20;

&#x20; 이 외에도 정사각형이 아닌 다각형으로 쿼리할 수 있는 **geo\_polygon** 쿼리가 있습니다. 이 책에서 geo\_polygon 쿼리는 다루지 않으니 [공식 도큐먼트](https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-dsl-geo-polygon-query.html)를 확인 하시기 바랍니다. 앞에서 배운 쿼리들을 이해 하셨으면 공식 도큐먼트를 참고 해서 어렵지 않게 사용이 가능 할 것입니다.


# 7.2.7 기타 필드 타입 - IP, Range, Binary

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; 지금까지 문자열, 숫자, 날짜, 불리언, 객체, 위치정보 들을 저장하는 필드 타입들을 살펴보았습니다. 이 외에도 Elasticsearch에는 다음과 같은 추상화 된 데이터 타입들이 있습니다.

### IP

&#x20; IP 주소 형식을 저장합니다. 매핑은 `"type": "ip"` 으로 선언합니다. 값은 `"192.168.1.1"` 같은 **IPv4** 형식과 `"0:0:0:0:0:ffff:c0a8:105"` 같은 **IPv6** 형식을 문자열 처럼 입력합니다.

### 범위(Range)

&#x20; 숫자나 날짜, IP 등을 시작과 끝이 있는 2차원의 범위 형태로 저장합니다. 매팽의 "type" 에 선언 가능한 값은 **integer\_range**, **float\_range**, **long\_range**, **double\_range**, **date\_range**, **ip\_range** 들이 있습니다. 데이터의 범위는 다음과 같이 **gt**, **gte**, **lt**, **lte** 를 사용해서 지정합니다.

{% code title="integer\_range 와 date\_range 타입의 필드 선언" %}

```javascript
PUT my_range
{
  "mappings": {
    "properties": {
      "amount": {
        "type": "integer_range"
      },
      "days": {
        "type": "date_range"
      }
    }
  }
}
```

{% endcode %}

쿼리를 테스트 하기 위해 먼저 다음 도큐먼트를 입력하겠습니다.

{% code title="integer\_range, date\_range 타입의 값을 가진 도큐먼트 입력" %}

```javascript
PUT my_range/_doc/1
{
  "amount": {
    "gte": 19,
    "lt": 28
  },
  "days": {
    "gt": "2019-06-01T09:00:00",
    "lt": "2019-06-20"
  }
}
```

{% endcode %}

&#x20; Range 필드의 쿼리는 일반적인 숫자나 날짜 처럼 range 쿼리를 사용합니다. 다만 범위 데이터를 range 쿼리로 검색 할 때는 추가로 **relation** 옵션의 값을 입력해야 하며 입력하지 않으면 오류가 납니다. relation 옵션에 지정 가능한 값은 **within**, **contains**, **intersects** 3가지가 있습니다.

* within : 도큐먼트 범위 값이 쿼리한 범위 안에 완전히 포함되는 도큐먼트들을 가져옵니다.
* contains : within과 반대로 쿼리 범위가 도큐먼트 범위 값 안에 완전히 포함되는 도큐먼트들을 가져옵니다.
* Intersects : 도큐먼트 범위 값과 쿼리 범위에 공통적인 부분이 있는 도큐먼트들을 가져옵니다.

사용 예제는 다음과 같습니다.

{% tabs %}
{% tab title="request" %}
{% code title=""relation": "intersects" 으로 range 쿼리" %}

```javascript
GET my_range/_search
{
  "query": {
    "range": {
      "amount": {
        "gte": "16",
        "lte": "25",
        "relation": "intersects"
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title=""relation": "intersects" 으로 range 쿼리 결과" %}

```javascript
{
  "took" : 950,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my_range",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "amount" : {
            "gte" : 19,
            "lt" : 28
          },
          "days" : {
            "gt" : "2019-06-01T09:00:00",
            "lt" : "2019-06-20"
          }
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 다른 값을 가진 도큐먼트를 더 입력하고 relation 값을 contains, within 으로 변경 해 가면서 더 실습 해 보시기 바랍니다.&#x20;

&#x20; 여행이나 출장 정보를 담은 도큐먼트가 있다면 시작일, 종료일 두 필드를 지정하는 대신 출장기간 이라는 date\_range 타입의 단일 필드로 값을 저장해서 더욱 편하고 유용하게 사용할 수 있습니다.

### Binary

&#x20; `"type": "binary"` 로 지정해서 시스템 파일이나 이미지 정보 같은 바이너리 값을 저장할 수 있습니다. binary 필드는 기본적으로 색인이 되지 않아 검색이나 집계가 불가능하고 \_source에만 남아 있습니다.

{% hint style="info" %}
바이너리 정보들은 일반적으로 용량이 크기 때문에 elasticsearch 도큐먼트에 저장하는 것은 불필요한 저장소와 통신 데이터의 낭비가 될 수 있습니다. 가능하면 바이너리 데이터의 저장은 S3 또는 HDFS 같은 저렴한 저장소를 이용하고 elasticsearch 도큐먼트에는 해당 자원에 접근 가능한 키 또는 URL 등만 저장해서 따로 가져오도록 하는 것이 바람직합니다.
{% endhint %}

{% hint style="warning" %}
지금까지 설명한 필드 외에도 수많은 종류의 필드 타입들이 있습니다. 새 버전이 나올 때 마다 새로운 필드 타입들이 추가되거나 기존에 있던 타입들의 사용이 만료되고 있기 때문에 항상 공식 도큐먼트를 잘 참고하시기 바랍니다.
{% endhint %}


# 7.3 멀티 (다중) 필드 - Multi Field

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Elasticsearch 의 도큐먼트에는 하나의 필드값만 있지만 이 필드의 값을 여러 개의 역 색인 및 doc\_values 들로 저장할 수 있는 다중 필드, 즉 **멀티 필드** 기능이 있습니다. 사실 이 내용은 앞에서 이미 여러 번 나왔습니다. 사용 방법은 다음과 같이 매핑에서 필드명 아래에 `"fields" : { }` 항목에서 다시 새로운 필드를 정의하고 설정합니다.

{% code title="멀티 필드 설정" %}

```javascript
PUT my_index
{
  "mappings": {
    "properties": {
      "<필드명1>": {
        "type": "text",
        "fields": {
          "<필드명2>": {
            "type": "<타입>"
          }
        }
      }
    }
  }
}
```

{% endcode %}

&#x20; 보통은 **text** 타입 아래에 **keyword** 타입을 같이 정의하기 위해서 사용됩니다. 다이나믹 매핑으로 문자열 값이 입력되면 자동으로 이런 모양으로 생성됩니다. 이 외에도 하나의 텍스트 필드에 **여러 개의 애널라이저를 적용**하기 위해서도 사용할 수 있습니다. 다음은 **my\_index** 인덱스의 **message** 필드에 서로 다른 애널라이저들을 사용하는 **english**, **nori** 멀티 필드를 정의하는 예제입니다.

{% code title="english, nori\_analyzer 를 사용하는 message 의 멀티필드 정의" %}

```javascript
PUT my_index
{
  "settings": {
    "analysis": {
      "analyzer": {
        "nori_analyzer": {
          "tokenizer": "nori_tokenizer"
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "message": {
        "type": "text",
        "fields": {
          "english": {
            "type": "text",
            "analyzer": "english"
          },
          "nori": {
            "type": "text",
            "analyzer": "nori_analyzer"
          }
        }
      }
    }
  }
}
```

{% endcode %}

{% hint style="warning" %}
nori\_analyzer 는 기본적으로 제공되는 애널라이저가 아니기 때문에 `"settings" : { }` 에서 만들어 줘야 합니다.
{% endhint %}

위와 같이 매핑을 정의하면 도큐먼트에는 **message** 필드값만 있어도 **message**, **message.english**, **message.nori** 총 3개의 역 색인이 생성됩니다. 위의 인덱스에 `{ "message": "My favorite 슈퍼영웅 is Iron Man" }` 이라는 값을 입력하면 다음과 같이 3개의 역 색인이 생성됩니다.

![message, message.english, message.nori 멀티 필드 역 색인](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-Lpef-ryjC_001Cv0nvu%2F-LpejgfvBL7HA0r72rd-%2F07-14.png?alt=media\&token=c2b5f2b3-a4f9-4c54-8719-d0b4bc3ba90e)

&#x20; **message.english** 에는 "favorite" 가 형태소 분석이 되어 **"favorit"** 로 저장되었습니다. **message.nori** 에는 "슈퍼영웅" 이 한글로 분석되어 **"슈퍼"**, **"영웅"** 으로 분리되어 저장되었습니다. 즉 match 쿼리를 할 때 `"messages": "영웅"` 으로는 검색이 안되지만 `"messages.nori": "영웅"` 으로 검색하면 검색이 됩니다.

{% tabs %}
{% tab title="request" %}
{% code title="message 역 색인에서 "영웅" 검색" %}

```javascript
GET my_index/_search
{
  "query": {
    "match": {
      "message": "영웅"
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="message 역 색인에서 "영웅" 검색 결과" %}

```javascript
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="request" %}
{% code title="message.nori 역 색인에서 "영웅" 검색" %}

```javascript
GET my_index/_search
{
  "query": {
    "match": {
      "message.nori": "영웅"
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="message.nori 역 색인에서 "영웅" 검색 결과" %}

```javascript
{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.2876821,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.2876821,
        "_source" : {
          "message" : "My favorite 슈퍼영웅 is Iron Man"
        }
      }
    ]
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 멀티 필드는 한 필드에 여러 애널라이저를 적용해야 하는 경우, 특히 다국어로 씌여진 도큐먼트를 분석해야 할 때 매우 유용합니다.

&#x20; 이번 장에서는 인덱스가 가지는 **settings** 와 **mappings** 의 설정 방법, 그리고 여러 종류의 **필드 타입** 들에 대해서 알아보았습니다. 많은 내용을 다루었지만 아직도 설명하지 못한 인덱스의 또다른 많은 설정들이 있습니다. 항상 공식 도큐먼트를 같이 참고 하시기 바랍니다.


# 8. 집계 - Aggregations

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Elasticsearch 는 검색엔진으로 개발되었지만 지금은 로그분석을 비롯해 다양한 목적의데이터 시스템으로 사용되고 있습니다. Elasticsearch가 이렇게 다양한 용도로 활용이 될 수 있는 이유는 데이터를 단순히 검색만 하는 것이 아니라 여러가지 연산을 할 수 있는 **Aggregation** 기능이 있기 때문입니다. Kibana 에서는 다음과 같이 바 차트, 파이 차트 등으로 데이터를 시각화 할 수 있는데 여기서 사용하는 것이 이 기능입니다.

![Kibana 화면 - 출처: https://www.elastic.co/products/kibana](https://2678746270-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Ln04DaYZaDjdiR_ZsKo%2F-LpfCc0NI1q5MYq5T0-G%2F-LpfCqhIQGw7sORFqRp2%2F08-01.png?alt=media\&token=86c81a51-19fc-464d-b1cc-b972fab55440)

&#x20; Aggregation 은 번역하면 "집계" 라는 뜻 이지만 Elasticsearch 의 기능명 이기 때문에 보통 Elastic Stack 관련 세미나 또는 블로그 포스트에서는 원문대로 **aggregation** 혹은 **애그리게이션** 으로 많이 표현합니다. 이 책에서도 **aggregation** 또는 **aggs** 로 표현하도록 하겠습니다.

&#x20; Aggregation 의 사용 방법은 다음과 같습니다. **\_search API** 에서 query 문과 같은 수준에 지정자 `aggregations` 또는 `aggs`를 명시하고 그 아래 임의의 aggregation 이름을 입력한 뒤 사용할 aggregation 종류와 옵션들을 명시합니다. 한번의 쿼리로 aggregation 여러 개를 입력할 수도 있습니다. 아래는 aggregation을 입력하는 예제입니다.

{% tabs %}
{% tab title="request" %}
{% code title="aggregations 입력" %}

```javascript
GET <인덱스명>/_search
{
  "query": {
    … <쿼리 구문> …
  },
  "aggs": {
    "<임의의 aggregation 1>": {
      "<aggregation 종류>": {
        … <aggreagation 구문> …
      }
    },
    "<임의의 aggregation 2>": {
      "<aggregation 종류>": {
        … <aggreagation 구문> …
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="aggregations 입력 결과" %}

```javascript
{
  "hits": {
… 쿼리 결과 (hit 된 도큐먼트 내용) …
  },
  "aggregations": {
    "<임의의 aggregation 1>": {
… aggregation 결과 …
    },
    "<임의의 aggregation 2>": {
… aggregation 결과 …
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; Aggregation 에는 크게 **Metrics** 그리고 **Bucket** 두 종류가 있습니다. Aggregations 구문이나 옵션에 metrics 이거나 bucket 이라고 따로 명시를 하지는 않습니다. Aggregation 종류들 중 숫자 또는 날짜 필드의 값을 가지고 계산을 하는 aggregation 들을 metrics aggregation 이라고 분류하고, 범위나 keyword 값 등을 가지고 도큐먼트들을 그룹화 하는 aggregation 들을 bucket aggregation 이라고 분류 합니다.

&#x20; 다음에 주로 사용되는 **Metrics** 와 **Bucket** Aggregations 들을 설명하기 위해 아래의 데이터를 먼저 **my\_stations** 인덱스에 입력하도록 하겠습니다. 처음 한 줄을 카피 해서 필드명은 그대로 두고 필드값만 수정하면 약간 더 수월하게 입력할 수 있습니다.

{% code title="테스트를 위한 my\_stations 인덱스에 데이터 입력" %}

```javascript
PUT my_stations/_bulk
{"index": {"_id": "1"}}
{"date": "2019-06-01", "line": "1호선", "station": "종각", "passangers": 2314}
{"index": {"_id": "2"}}
{"date": "2019-06-01", "line": "2호선", "station": "강남", "passangers": 5412}
{"index": {"_id": "3"}}
{"date": "2019-07-10", "line": "2호선", "station": "강남", "passangers": 6221}
{"index": {"_id": "4"}}
{"date": "2019-07-15", "line": "2호선", "station": "강남", "passangers": 6478}
{"index": {"_id": "5"}}
{"date": "2019-08-07", "line": "2호선", "station": "강남", "passangers": 5821}
{"index": {"_id": "6"}}
{"date": "2019-08-18", "line": "2호선", "station": "강남", "passangers": 5724}
{"index": {"_id": "7"}}
{"date": "2019-09-02", "line": "2호선", "station": "신촌", "passangers": 3912}
{"index": {"_id": "8"}}
{"date": "2019-09-11", "line": "3호선", "station": "양재", "passangers": 4121}
{"index": {"_id": "9"}}
{"date": "2019-09-20", "line": "3호선", "station": "홍제", "passangers": 1021}
{"index": {"_id": "10"}}
{"date": "2019-10-01", "line": "3호선", "station": "불광", "passangers": 971}

```

{% endcode %}


# 8.1 메트릭 - Metrics Aggregations

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

### min, max, sum, avg

&#x20; 가장 흔하게 사용되는 metrics aggregations 은 **min**, **max**, **sum**, **avg** aggregation 입니다. 순서대로 명시한 필드의 **최소**, **최대**, **합**, **평균** 값을 가져오는 aggregation 입니다. 다음은 sum aggregation을 이용해서 my\_stations 에 있는 전체 데이터의 **passangers** 필드값의 합계를 가져오는 예제입니다.

{% tabs %}
{% tab title="request" %}
{% code title="my\_stations 인덱스의 passangers 필드 합 (sum) 을 가져오는 aggs" %}

```javascript
GET my_stations/_search
{
  "size": 0,
  "aggs": {
    "all_passangers": {
      "sum": {
        "field": "passangers"
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="my\_stations 인덱스의 passangers 필드 합 (sum) 을 가져오는 aggs 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "all_passangers" : {
      "value" : 41995.0
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; **min**, **max**, **avg** 들도 사용 방법은 동일하니 한번 해 보시기 바랍니다.

{% hint style="info" %}
aggregations 만 사용하는 경우에는 `"size": 0` 을 지정 하면 "hits": \[ ] 에 불필요한 도큐먼트 내용이 나타나지 않아 보기에도 편하고 도큐먼트를 fetch 해 오는 과정을 생략할 수 있어 쿼리 성능도 좋아집니다.
{% endhint %}

&#x20; aggregation해 오는 도큐먼트들은 같이 입력된 query 문의 영향을 받습니다. 다음은 my\_stations에서 `"station": "강남"` 인 도큐먼트들의 합계를 가져오는 예제입니다.

{% tabs %}
{% tab title="request" %}
{% code title="stations 값이 "강남" 인 도큐먼트들의 passangers 필드 합 (sum) 을 가져오는 aggs" %}

```javascript
GET my_stations/_search
{
  "query": {
    "match": {
      "station": "강남"
    }
  },
  "size": 0,
  "aggs": {
    "gangnam_passangers": {
      "sum": {
        "field": "passangers"
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="stations 값이 "강남" 인 도큐먼트들의 passangers 필드 합 (sum) 을 가져오는 aggs 결과" %}

```javascript
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 5,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "gangnam_passangers" : {
      "value" : 29656.0
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 처음 쿼리와 다르게 전체 hits 결과는 5개이고 aggregation 결과도 **41995** 에서 **29656** 으로 줄어든 것을 확인할 수 있습니다.

### stats

&#x20; **min**, **max**, **sum**, **avg** 값을 모두 가져와야 한다면 다음과 같이 **stats** aggregation을 사용하면 위 4개의 값 모두와 count 값을 한번에 가져옵니다.

{% tabs %}
{% tab title="request" %}
{% code title="stats 로 passangers 필드의 min, max, sum, avg 값을 가져오는 aggs" %}

```javascript
GET my_stations/_search
{
  "size": 0, 
  "aggs": {
    "passangers_stats": {
      "stats": {
        "field": "passangers"
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="stats 로 passangers 필드의 min, max, sum, avg 값을 가져오는 aggs 결과" %}

```javascript
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "passangers_stats" : {
      "count" : 10,
      "min" : 971.0,
      "max" : 6478.0,
      "avg" : 4199.5,
      "sum" : 41995.0
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

### cardinality

&#x20; 필드의 값이 모두 몇 종류인지 분포값을 알려면 **cardinality** aggregation을 사용해서 구할 수 있습니다. Cardinality 는 일반적으로 text 필드에서는 사용할 수 없으며 **숫자** 필드나 **keyword**, **ip** 필드 등에 사용이 가능합니다. 사용자 접속 로그에서 IP 주소 필드를 가지고 실제로 접속한 사용자가 몇명인지 파악하는 등의 용도로 주로 사용됩니다. 다음은 my\_stations 인덱스에서 **line** 필드의 값이 몇 종류인지를 계산하는 예제입니다.

{% tabs %}
{% tab title="request" %}
{% code title="line 필드의 값이 몇 종류인지를 가져오는 aggs" %}

```javascript
GET my_stations/_search
{
  "size": 0,
  "aggs": {
    "uniq_lines ": {
      "cardinality": {
        "field": "line.keyword"
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="line 필드의 값이 몇 종류인지를 가져오는 aggs 결과" %}

```javascript
{
  "took" : 15,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "uniq_lines " : {
      "value" : 3
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 위 쿼리 결과 `"uniq_lines " : { "value" : 3 }` 처럼 실제로 line 필드에는 "1호선", "2호선", "3호선" 총 3 종류의 값들이 있습니다.

### percentiles, percentile\_ranks

&#x20; 값들을 백분위 별로 보기 위해서 **percentiles** aggregation 의 사용이 가능합니다. 먼저 passangers 필드에 percentiles aggregation 적용 한 것을 확인 해 보겠습니다.

{% tabs %}
{% tab title="request" %}
{% code title="passangers 필드의 백분위를 가져오는 aggs" %}

```javascript
GET my_stations/_search
{
  "size": 0,
  "aggs": {
    "pass_percentiles": {
      "percentiles": {
        "field": "passangers"
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="passangers 필드의 백분위를 가져오는 aggs 결과" %}

```javascript
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "pass_percentiles" : {
      "values" : {
        "1.0" : 971.0000000000001,
        "5.0" : 971.0,
        "25.0" : 2314.0,
        "50.0" : 4766.5,
        "75.0" : 5821.0,
        "95.0" : 6478.0,
        "99.0" : 6478.0
      }
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; percentiles aggregation은 디폴트로 **1%**, **5%**, **25%**, **50%**, **75%**, **95%**, **99%** 구간에 위치 해 있는 값들을 표시 해 줍니다. 백분위 구간을 직접 지정하고 싶으면 **percents** 옵션을 이용해서 지정이 가능합니다. 다음은 **20%**, **60%**, **80%** 백분위의 값을 가져오는 percentiles aggregation 입니다.

{% tabs %}
{% tab title="request" %}
{% code title="passangers 필드의 백분위를 지정해서 가져오는 aggs" %}

```javascript
GET my_stations/_search
{
  "size": 0,
  "aggs": {
    "pass_percentiles": {
      "percentiles": {
        "field": "passangers",
        "percents": [ 20, 60, 80 ]
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="passangers 필드의 백분위를 지정해서 가져오는 aggs 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "pass_percentiles" : {
      "values" : {
        "20.0" : 1667.5,
        "60.0" : 5568.0,
        "80.0" : 6021.0
      }
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; percentile\_ranks aggregation을 이용하면 반대로 값을 입력해서 그 값이 위치 해 있는 백분위를 볼 수 있습니다.

{% tabs %}
{% tab title="request" %}
{% code title="passangers 필드의 값을 지정해서 백분위를 가져오는 aggs" %}

```javascript
GET my_stations/_search
{
  "size": 0,
  "aggs": {
    "pass_percentile_ranks": {
      "percentile_ranks": {
        "field": "passangers",
        "values": [ 1000, 3000, 6000 ]
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="passangers 필드의 값을 지정해서 백분위를 가져오는 aggs 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "pass_percentile_ranks" : {
      "values" : {
        "1000.0" : 10.059568131049886,
        "3000.0" : 29.218263576617087,
        "6000.0" : 79.1549295774648
      }
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20;  위 쿼리에서 입력한 passangers 값 **1000**, **3000**, **6000**이 각각 **10.059...%**, **29.218...%**, **79.154...%** 백분위에 위치 한 결과를 확인할 수 있습니다. **percentile\_ranks** aggregation 은 전체 수험생의 성적 중에서 특정 점수가 상위 몇% 에 있는지 등을 파악할 때 매우 편리하게 사용할 수 있습니다.


# 8.2 버킷 - Bucket Aggregations

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Bucket aggregation 은 주어진 조건으로 분류된 **버킷** 들을 만들고, 각 버킷에 소속되는 도큐먼트들을 모아 **그룹으로 구분**하는 것입니다. 각 버킷 별로 포함되는 도큐먼트의 개수는 **doc\_count** 값에 기본적으로 표시가 되며 각 버킷 안에 metrics aggregation 을 이용해서 다른 계산들도 가능합니다. 주로 사용되는 bucket aggregation 들은 **Range**, **Histogram**, **Terms** 등이 있습니다.

### range

&#x20; **range** 는 숫자 필드 값으로 범위를 지정하고 각 범위에 해당하는 버킷을 만드는 aggregation 입니다. **field** 옵션에 해당 필드의 이름을 지정하고 **ranges** 옵션에 배열로 **from**, **to** 값을 가진 오브젝트 값을 나열해서 범위를 지정합니다. 다음은 passangers 값이 각각 1000 미만, 1000\~4000 사이, 4000 이상 인 버킷들을 생성하는 예제입니다.

{% tabs %}
{% tab title="request" %}
{% code title="range aggs 를 이용해서 passangers 필드의 값을 버킷으로 구분" %}

```javascript
GET my_stations/_search
{
  "size": 0,
  "aggs": {
    "passangers_range": {
      "range": {
        "field": "passangers",
        "ranges": [
          {
            "to": 1000
          },
          {
            "from": 1000,
            "to": 4000
          },
          {
            "from": 4000
          }
        ]
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="range aggs 를 이용해서 passangers 필드의 값을 버킷으로 구분한 결과" %}

```javascript
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "passangers_range" : {
      "buckets" : [
        {
          "key" : "*-1000.0",
          "to" : 1000.0,
          "doc_count" : 1
        },
        {
          "key" : "1000.0-4000.0",
          "from" : 1000.0,
          "to" : 4000.0,
          "doc_count" : 3
        },
        {
          "key" : "4000.0-*",
          "from" : 4000.0,
          "doc_count" : 6
        }
      ]
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 각각의 버킷을 구분하는 key 값은 `"from-to"` 형태로 생성됩니다. 위 쿼리 결과에서 각각의 `"*-1000.0"`, `"1000.0-4000.0"`, `"key" : "4000.0-*"` 버킷에 속한 도큐먼트의 개수가 **1**, **3**, **6**개 인 것을 확인할 수 있습니다.

{% hint style="info" %}
aggs 에 설정한 **from** 은 **이상** 즉 버킷에 포함이고 **to** 는 **미만** 으로 버킷에 포함하지 않습니다. 예를 들어 필드 값이 **200** 인 도큐먼트는 `"key" : "100-200"` 버킷에는 포함되지 않고 `"key" : "200-300"` 버킷에는 포함됩니다.
{% endhint %}

### histogram

&#x20; histogram 도 range 와 마찬가지로 숫자 필드의 범위를 나누는 aggs 입니다. 앞에서 본 range 는 from 과 to 를 이용해서 각 버킷의 범위를 지정했습니다. histogram 은 from, to 대신 **interval** 옵션을 이용해서 주어진 간격 크기대로 버킷을 구분합니다. 다음은 passangers 필드에 간격이 2000인 버킷들을 생성하는 예제입니다.

{% tabs %}
{% tab title="request" %}
{% code title="histogram aggs 를 이용해서 passangers 필드의 값을 버킷으로 구분" %}

```javascript
GET my_stations/_search
{
  "size": 0,
  "aggs": {
    "passangers_his": {
      "histogram": {
        "field": "passangers",
        "interval": 2000
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="histogram aggs 를 이용해서 passangers 필드의 값을 버킷으로 구분한 결과" %}

```javascript
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "passangers_his" : {
      "buckets" : [
        {
          "key" : 0.0,
          "doc_count" : 2
        },
        {
          "key" : 2000.0,
          "doc_count" : 2
        },
        {
          "key" : 4000.0,
          "doc_count" : 4
        },
        {
          "key" : 6000.0,
          "doc_count" : 2
        }
      ]
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 각각의 버킷을 구분하는 key 에는 값의 최소값이 표시됩니다. 위 예제에서 `0`, `2000`, `4000`, `6000` 인 버킷들이 생성되었고 각 버킷들에 포함되는 도큐먼트 개수가 표시된 것을 볼 수 있습니다. key 값은 range 와 마찬가지로 key 로 지정된 값이 버킷에 이상(포함) / 미만(포함하지 않음) 으로 지정됩니다.

### date\_range, date\_histogram

&#x20; **range** 와 **histogram** aggs 처럼 숫자 외에도 날짜 필드를 이용해서 범위별로 버킷의 생성이 가능합니다. 이 때 사용되는 **date\_range**, **date\_histogram** aggs 들은 특히 시계열 데이터에서 날짜별로 값을 표시할 때 매우 유용합니다. **date\_range** 는 **ranges** 옵션에 `{"from": "2019-06-01", "to": "2016-07-01"}` 와 같이 입력하며 **date\_histogram** 은 **interval** 옵션에 `day`, `month`, `week` 와 같은 값들을 이용해서 날짜 간격을 지정할 수 있습니다. 다음은 **date\_histogram** 으로 **date** 필드를 **1개월** 간격으로 구분하는 예제입니다.

{% tabs %}
{% tab title="request" %}
{% code title="date\_histogram 을 이용해서 date 값을 1개월 간격의 버킷으로 구분" %}

```javascript
GET my_stations/_search
{
  "size": 0,
  "aggs": {
    "date_his": {
      "date_histogram": {
        "field": "date",
        "interval": "month"
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="date\_histogram 을 이용해서 date 값을 1개월 간격의 버킷으로 구분한 결과" %}

```javascript
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "date_his" : {
      "buckets" : [
        {
          "key_as_string" : "2019-06-01T00:00:00.000Z",
          "key" : 1559347200000,
          "doc_count" : 2
        },
        {
          "key_as_string" : "2019-07-01T00:00:00.000Z",
          "key" : 1561939200000,
          "doc_count" : 2
        },
        {
          "key_as_string" : "2019-08-01T00:00:00.000Z",
          "key" : 1564617600000,
          "doc_count" : 2
        },
        {
          "key_as_string" : "2019-09-01T00:00:00.000Z",
          "key" : 1567296000000,
          "doc_count" : 3
        },
        {
          "key_as_string" : "2019-10-01T00:00:00.000Z",
          "key" : 1569888000000,
          "doc_count" : 1
        }
      ]
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; date\_histogram의 결과에서는 버킷별로 key 에 시작 날짜가 **epoch\_millis** 값으로 표시됩니다. 그리고 **key\_as\_string** 에 **ISO8601** 형태로도 함께 표시됩니다.

{% hint style="warning" %}
7.2 버전 부터는 interval 옵션이 사용 종료 권장(depricated) 되고 대신 **fixed\_interval** 과 **calendar\_interval** 으로 나누어 지게 되었습니다. year, quarter, month, week, 같은 달력 기준의 값은 `"calendar_interval" : "month"` 로 입력을 하고, 30일 처럼 정확히 구분되는 날짜들은 `"fixed_interval" : "30d"` 로 지정을 해야 합니다.
{% endhint %}

### terms

&#x20; 앞에서 살펴 본 **(date\_)range**, **histogram** 은 모두 숫자, 날짜를 가지고 구간을 나누는 aggregation 이었습니다. **terms** aggregation 은 **keyword** 필드의 문자열 별로 버킷을 나누어 집계가 가능합니다. keyword 필드 값으로만 사용이 가능하며 분석된 text 필드는 일반적으로는 사용이 불가능합니다. 다음은 my\_stations 인덱스에서 **station.keyword** 필드를 기준으로 버킷들을 만드는 예제입니다.

{% tabs %}
{% tab title="request" %}
{% code title="terms 을 이용해서 station 값을 별로 버킷 생성" %}

```javascript
GET my_stations/_search
{
  "size": 0,
  "aggs": {
    "stations": {
      "terms": {
        "field": "station.keyword"
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="terms 을 이용해서 station 값을 별로 버킷 생성한 결과" %}

```javascript
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "stations" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "강남",
          "doc_count" : 5
        },
        {
          "key" : "불광",
          "doc_count" : 1
        },
        {
          "key" : "신촌",
          "doc_count" : 1
        },
        {
          "key" : "양재",
          "doc_count" : 1
        },
        {
          "key" : "종각",
          "doc_count" : 1
        },
        {
          "key" : "홍제",
          "doc_count" : 1
        }
      ]
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; **terms** aggregation 에는 **field** 외에도 가져올 버킷의 개수를 지정하는 **size** 옵션이 있으며 디폴트 값은 **10** 입니다. 인덱스의 특정 keyword 필드에 있는 모든 값들을 종류별로 버킷을 만들면 가져와야 할 결과가 매우 많기 때문에 먼저 도큐먼트 개수 또는 주어진 metrics 연산 결과가 가장 많은 버킷 들을 샤드별로 계산해서 상위 몇개의 버킷들만 coordinate 노드로 가져오고, 그것들을 취합해서 결과를 나타냅니다. 이 과정은 검색의 query 그리고 fetch 과정과 유사합니다.


# 8.3 하위 - sub-aggregations

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Bucket Aggregation 으로 만든 버킷들 내부에 다시 `"aggs" : { }` 를 선언해서 또다른 버킷을 만들거나 Metrics Aggregation 을 만들어 사용이 가능합니다. 다음은 terms aggregation을 이용해서 생성한 **stations** 버킷 별로 **avg** aggregation을 이용해서 **passangers** 필드의 평균값을 계산하는 **avg\_psg\_per\_st** 을 생성하는 예제입니다.

{% tabs %}
{% tab title="request" %}
{% code title="terms aggs 아래에 avg aggs 사용" %}

```javascript
GET my_stations/_search
{
  "size": 0,
  "aggs": {
    "stations": {
      "terms": {
        "field": "station.keyword"
      },
      "aggs": {
        "avg_psg_per_st": {
          "avg": {
            "field": "passangers"
          }
        }
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="terms aggs 아래에 avg aggs 사용 결과" %}

```javascript
{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "stations" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "강남",
          "doc_count" : 5,
          "avg_psg_per_st" : {
            "value" : 5931.2
          }
        },
        {
          "key" : "불광",
          "doc_count" : 1,
          "avg_psg_per_st" : {
            "value" : 971.0
          }
        },
        {
          "key" : "신촌",
          "doc_count" : 1,
          "avg_psg_per_st" : {
            "value" : 3912.0
          }
        },
        {
          "key" : "양재",
          "doc_count" : 1,
          "avg_psg_per_st" : {
            "value" : 4121.0
          }
        },
        {
          "key" : "종각",
          "doc_count" : 1,
          "avg_psg_per_st" : {
            "value" : 2314.0
          }
        },
        {
          "key" : "홍제",
          "doc_count" : 1,
          "avg_psg_per_st" : {
            "value" : 1021.0
          }
        }
      ]
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; **stations** 버킷들 별로 **avg\_psg\_per\_st** 라는 aggregation이 실행된 것을 확인할 수 있습니다. 버킷 안에 또 다른 하위 버킷을 만드는 것도 가능합니다. 다음은 terms aggregation 을 이용해서 line.keyword 별로 **lines** 버킷을 만들고 그 안에 또다시 terms aggregation을 이용한 **stations\_per\_lines** 버킷을 만든 예제입니다.

{% tabs %}
{% tab title="request" %}
{% code title="terms aggs 아래에 하위 terms aggs 사용" %}

```javascript
GET my_stations/_search
{
  "size": 0,
  "aggs": {
    "lines": {
      "terms": {
        "field": "line.keyword"
      },
      "aggs": {
        "stations_per_lines": {
          "terms": {
            "field": "station.keyword"
          }
        }
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="terms aggs 아래에 하위 terms aggs 사용 결과" %}

```javascript
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "lines" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "2호선",
          "doc_count" : 6,
          "stations_per_lines" : {
            "doc_count_error_upper_bound" : 0,
            "sum_other_doc_count" : 0,
            "buckets" : [
              {
                "key" : "강남",
                "doc_count" : 5
              },
              {
                "key" : "신촌",
                "doc_count" : 1
              }
            ]
          }
        },
        {
          "key" : "3호선",
          "doc_count" : 3,
          "stations_per_lines" : {
            "doc_count_error_upper_bound" : 0,
            "sum_other_doc_count" : 0,
            "buckets" : [
              {
                "key" : "불광",
                "doc_count" : 1
              },
              {
                "key" : "양재",
                "doc_count" : 1
              },
              {
                "key" : "홍제",
                "doc_count" : 1
              }
            ]
          }
        },
        {
          "key" : "1호선",
          "doc_count" : 1,
          "stations_per_lines" : {
            "doc_count_error_upper_bound" : 0,
            "sum_other_doc_count" : 0,
            "buckets" : [
              {
                "key" : "종각",
                "doc_count" : 1
              }
            ]
          }
        }
      ]
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 각 호선별로 역 버킷이 생성된 것을 확인할 수 있습니다. 두 번째 하위 aggregations 안에 또 다른 metrics aggregations를 입력하는 것도 가능합니다.

{% hint style="danger" %}
하위 버킷이 깊어질수록 elasticsearch 가 하는 작업량과 메모리 소모량이 기하급수적으로 늘어나기 때문에 예상치 못한 오류를 발생 시킬수도 있습니다. 보통은 2레벨의 깊이 이상의 버킷은 생성하지 않는 것이 좋습니다.
{% endhint %}


# 8.4 파이프라인 - Pipeline Aggregations

이 문서의 허가되지 않은 무단 복제나 배포 및 출판을 금지합니다. 본 문서의 내용 및 도표 등을 인용하고자 하는 경우 출처를 명시하고 김종민(kimjmin\@gmail.com)에게 사용 내용을 알려주시기 바랍니다.

&#x20; Aggregation 중에는 다른 **metrics** aggregation의 결과를 새로운 입력으로 하는 **pipeline** aggregation이 있습니다. pipeline 에는 다른 버킷의 결과들을 다시 연산하는 **min\_bucket**, **max\_bucket**, **avg\_bucket**, **sum\_bucket**, **stats\_bucket**, 이동 평균을 구하는 **moving\_avg**, 미분값을 구하는 **derivative**, 값의 누적 합을 구하는 **cumulative\_sum** 등이 있습니다. Pipeline aggregation 은 `"buckets_path": "<버킷 이름>"` 옵션을 이용해서 입력 값으로 사용할 버킷을 지정합니다. 다음은 my\_stations 에서 **date\_histogram**을 이용해서 월별로 나눈 passangers 의 합계 sum을 다시 **cumulative\_sum**을 이용해서 누적값을 구하는 예제입니다.

{% tabs %}
{% tab title="request" %}
{% code title="passangers 의 값을 입력으로 받는 cumulative\_sum aggs 실행" %}

```javascript
GET my_stations/_search
{
  "size": 0,
  "aggs": {
    "months": {
      "date_histogram": {
        "field": "date",
        "interval": "month"
      },
      "aggs": {
        "sum_psg": {
          "sum": {
            "field": "passangers"
          }
        },
        "accum_sum_psg": {
          "cumulative_sum": {
            "buckets_path": "sum_psg"
          }
        }
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="passangers 의 값을 입력으로 받는 cumulative\_sum aggs 실행 결과" %}

```javascript
{
  "took" : 7,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "months" : {
      "buckets" : [
        {
          "key_as_string" : "2019-06-01T00:00:00.000Z",
          "key" : 1559347200000,
          "doc_count" : 2,
          "sum_psg" : {
            "value" : 7726.0
          },
          "accum_sum_psg" : {
            "value" : 7726.0
          }
        },
        {
          "key_as_string" : "2019-07-01T00:00:00.000Z",
          "key" : 1561939200000,
          "doc_count" : 2,
          "sum_psg" : {
            "value" : 12699.0
          },
          "accum_sum_psg" : {
            "value" : 20425.0
          }
        },
        {
          "key_as_string" : "2019-08-01T00:00:00.000Z",
          "key" : 1564617600000,
          "doc_count" : 2,
          "sum_psg" : {
            "value" : 11545.0
          },
          "accum_sum_psg" : {
            "value" : 31970.0
          }
        },
        {
          "key_as_string" : "2019-09-01T00:00:00.000Z",
          "key" : 1567296000000,
          "doc_count" : 3,
          "sum_psg" : {
            "value" : 9054.0
          },
          "accum_sum_psg" : {
            "value" : 41024.0
          }
        },
        {
          "key_as_string" : "2019-10-01T00:00:00.000Z",
          "key" : 1569888000000,
          "doc_count" : 1,
          "sum_psg" : {
            "value" : 971.0
          },
          "accum_sum_psg" : {
            "value" : 41995.0
          }
        }
      ]
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 위 결과에서 **accum\_sum\_psg** 결과에 **sum\_psg** 값이 다음과 같이 계속 누적되어 더해지고 있는 것을 확인할 수 있습니다.

* `"accum_sum_psg" : { "value" : 7726.0}` = 7726.0
* `"accum_sum_psg" : { "value" : 20425.0}` = 7726.0 + 12699.0
* `"accum_sum_psg" : { "value" : 31970.0}` = 7726.0 + 12699.0 + 11545.0
* ...

&#x20; 서로 다른 버킷에 있는 값들도 bucket\_path에 `>` 기호를 이용해서 `"부모>자녀"` 형태로 지정이 가능합니다. 다음은 sum\_bucket 을 이용해서 **mon>sum\_psg** 버킷에 있는 passangers 필드값의 합을 구하는 예제입니다.

{% tabs %}
{% tab title="request" %}
{% code title="다른 부모의 자녀 버킷에 있는 필드를 입력으로 받는 pipeline aggs" %}

```javascript
GET my_stations/_search
{
  "size": 0,
  "aggs": {
    "mon": {
      "date_histogram": {
        "field": "date",
        "interval": "month"
      },
      "aggs": {
        "sum_psg": {
          "sum": {
            "field": "passangers"
          }
        }
      }
    },
    "bucket_sum_psg": {
      "sum_bucket": {
        "buckets_path": "mon>sum_psg"
      }
    }
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="response" %}
{% code title="다른 부모의 자녀 버킷에 있는 필드를 입력으로 받는 pipeline aggs 실행 결과" %}

```javascript
{
  "took" : 4,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "mon" : {
      "buckets" : [
        {
          "key_as_string" : "2019-06-01T00:00:00.000Z",
          "key" : 1559347200000,
          "doc_count" : 2,
          "sum_psg" : {
            "value" : 7726.0
          }
        },
        {
          "key_as_string" : "2019-07-01T00:00:00.000Z",
          "key" : 1561939200000,
          "doc_count" : 2,
          "sum_psg" : {
            "value" : 12699.0
          }
        },
        {
          "key_as_string" : "2019-08-01T00:00:00.000Z",
          "key" : 1564617600000,
          "doc_count" : 2,
          "sum_psg" : {
            "value" : 11545.0
          }
        },
        {
          "key_as_string" : "2019-09-01T00:00:00.000Z",
          "key" : 1567296000000,
          "doc_count" : 3,
          "sum_psg" : {
            "value" : 9054.0
          }
        },
        {
          "key_as_string" : "2019-10-01T00:00:00.000Z",
          "key" : 1569888000000,
          "doc_count" : 1,
          "sum_psg" : {
            "value" : 971.0
          }
        }
      ]
    },
    "bucket_sum_psg" : {
      "value" : 41995.0
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

&#x20; 이번 장에서는 Elasticsearch가 텍스트 검색엔진을 넘어 데이터 분석 엔진으로서의 기능을 가능하게 해 준 **Aggregations** 에 대해서 알아보았습니다. Aggregations 에는 다양한 값들을 연산하는 **metrics**, 범위나 종류 별로 값들을 분리하는 **bucket**, 그리고 다른 aggregation의 결과를 입력으로 받아 새로운 연산을 수행하는 **pipeline** 이 있습니다. 이 장에서는 주로 사용되는 aggregation들 위주로 기본적인 사용 방법에 대해 설명했습니다. 지금까지 설명한 것 외에도 수많은 종류의 aggregation 들이 있으니 공식 도큐먼트에서 확인 하시기 바랍니다.


