본문 바로가기

Back-End

Elastic 가이드북과 함께 Elasticsearch 공부하기

반응형

Elastic 가이드북을 꼼꼼히 살펴보면서 Elasticsearch를 공부하고 사용법을 익히자! (이 가이드북은 Elastic Stack 7.x 버전을 기준으로 설명하고 있다.)

 

(➕) 2021.10.20 - [분류 전체보기] - Elasticsearch 설명서와 함께하는 Elasticsearch 기본 개념 살펴보기

 

 

Intro

데이터 홍수의 세상에서 데이터 분석 및 처리하는 일이 중요해졌다. Elasticsearch가 나온 지도 벌써 9년이 넘었고, 현재 가장 인기 있는 오픈소스 검색엔진이 되었다.

처음에는 전문 검색 엔진(Full-text search engine)으로 개발되었지만, 보안, 로그분석, 전문분석 등 다양한 영역으로 확장하였고 현재는 Kibana, Logstash, Beats와 함께 다양한 전문 분야에서 문제를 해결하고 있다. 2013년만 해도 1.0 버전이 나오기 전이었는데 2019년 7.x 버전을 발표하면서 기업용 솔루션으로 완전히 자리 잡은 것을 볼 수 있다.

 

 

 

1. 서문

1-1. Elastic Stack 소개

Elastic Stack이란?: Elastic Stack의 역사

2004년 샤이 배논(Shay Banon)이 요리 공부를 시작한 아내를 위해 레시피 검색 프로그램을 만들기 위해서 'Compass'라는 오픈소스 검색엔진을 개발하였다. 레시피 검색 프로그램에 아파치 루씬(Apache Lucene)을 적용하려고 했지만 루씬이 가진 한계를 보완하기 위해 새로운 검색엔진을 만들게 된 것이다.

2010년에 샤이는 Compass를 Elasticsearch라고 이름을 바꾸고 오픈소스로 공개하면서 많은 인기를 얻으며 급속도로 성장하기 시작했다.

Elasticsearch 프로젝트는 2012년에 창시자 샤이 배논과 함께 스티븐 셔르만(Steve Schuurman), 우리 보네스(Uri Boness) 그리고 아파치 루씬 커미터인 사이먼 윌너(Simon Willnauer) 4인의 멤버로 인해 네덜란드 암스텔담에서 처음 회사를 설립되었고, 현재는 주 본사인 네덜란드를 비롯하여 전 세계에 직원들이 분포하고 있다.

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

 

1-2. Elasticsearch

Elasticsearch이란?

  • Elastic Stack에서 중심이며, 가장 중요한 역할을 한다
  • 기본적으로 모든 데이터를 색인하여 저장하고 검색, 집계 등을 수행하며 결과를 클라이언트 또는 다른 프로그램으로 전달하여 동작하게 한다.
  • 뛰어난 검색 능력과 대규모 분산 시스템을 구축할 수 있는 다양한 기능들을 제공한다.
  • 설치 과정과 사용 방법이 쉽고 간편하다.
  • 기존 관계 데이터베이스 시스템에서 다루기 어려운 전문검색(Full Text Search)와 점수 기반의 다양한 정확도 알고리즘, 실시간 분석 등의 구현이 가능하다.
  • 다양한 플러그인들으로 쉽게 기능을 확장할 수 있다.
  • 아마존 웹 서비스(AWS), 마이크로소프트 애저(MS Azure) 같은 클라우드 서비스와, 하둡(Hadoop) 플랫폼들과 연동할 수 있다.

 

Elaistcsearch의 특징

1. 오픈 소스

Elasticsearch 핵심 기능들은 Apache 2.0 라이센스로 배포되고 있으며 Elastic Stack의 모든 제품들은 깃헙 레퍼지토리에서 소스를 볼 수 있다. 

루씬이 자바로 만들어졌기 때문에 Elasticsearch도 자바로 코딩되어 있다. 루씬은 하둡을 개발한 더그 커팅(Doug Cutting)에 의해 처음 만들어졌지만 Elasticsearch 엔지니어들 중에 루씬 커미터들이 다수 있어서 루씬을 깊이있게 다루고 있다. 실제로 Elastic 사의 루씬 커미터 개발자들이 루씬 프로젝트의 절반 이상의 기능을 개발하는데에 기여하고 있다.

2. 실시간 분석 (real-time)

현재 대용량 데이터 분석에 가장 널리 사용되고 있는 하둡(Hadoop) 플랫폼 위에서 실행되는 Pig, Hive와 같은 다양한 맵 리듀서(Map reducer)들이다. 하둡은 기본적으로 분석에 사용될 소스 데이터들과 프로그램을 올려놓고 분석을 실행하여 결과가 나오도록 한다.

Elasticsearch는 하둡과 달리 Elastisearch 클러스터가 실행되고 있는 동안에는 계속해서 데이터가 입력되고, 이와 동시에 실시간에 가까운 속도로 색인된 데이터의 검색 및 집계할 수 있다.

3. 전문(full text) 검색 엔진

루씬은 기본적으로 역파일 색인(inverted file index) 구조로 데이터를 저장한다. 이러한 특성을 전문 검색(Full Text Search)이라고 한다.

루씬을 사용하는 JSON 문서 기반인 Elasticsearch도 마찬가지로 색인된 모든 데이터를 inverted file index 구조로 저장하지만, 사용자 관점에서는 JSON 형식으로 데이터를 전달한다. JSON 형식은 간결하고 다루기 편한 구조이므로 색인할 대상 문서를 가공하거나 다른 클라이언트 프로그램과 연동하기에 용이하다. 또한 key-value 형식이 아니라 문서 기반으로 되어 있기 때문에 많은 복잡한 정보를 포함하는 형식의 문서를 그대로 저장할 수 있고 사용자에게 보다 직관적으로 이해할 수 있다. Elasticsearch에서 쿼리문이나 쿼리에 대한 결과도 모두 JSON 형식이다.

다만 Elasticsearch에서는 JSON 형식만을 지원하기 때문에 입력할 데이터를 JSON 형식으로 가공해야 한다. CSV, Apach log, syslog 등 널리 사용되는 형식들은 Logstash에서 변환할 수 있다.

4. RESTful API

Elasticsearch는 REST API를 지원하며 모든 데이터 조회, 입력, 삭제를 http 프로토콜을 통해 REST API로 처리한다.

5. 멀티테넌시 (multitenancy)

Elasticsearch의 데이터들은 인덱스(Index)라는 집합 단위로 구성되고, 서로 다른 저장소에 분산되어 저장된다. 서로 다른 인덱스들을 별도의 커넥션 없이도 하나의 쿼리로 검색할 수 있는데, 이러한 특징을 '멀티테넌시'라 한다.

 

1-3. Logstash

Logstash란?

원래는 다양한 데이터 수집 및 저장을 위해 개발된 프로젝트였다. 데이터 색인(indexing), 검색 기능만 제공하던 Elasticsearch는 데이터 수집을 위한 도구가 필요했는데, 떄마침 Logstash가 출력 API로 Elasticsearch를 지원하기 시작하면서 많은 곳에서 Elasticsearch의 입력 수단으로 Logstash를 사용하기 시작했다. 그 후 서로 통합의 필요성을 느끼고 Logstash가 Elastic에 합류하게 되면서 하나의 스택으로 출범하게 되었다.

 

구성

Logstash는 JRuby로 되어있다. 루비 코드로 개발되어 자바의 런타임 머신 위에서 돌아간다.

 

데이터 처리 과정

크게 다음 과정을 거친다.

입력(Inputs) ➡ 필터(Filters) ➡ 출력(Outputs)
  • 입력: 다양한 데이터 저장소로부터 데이터를 입력 받음
  • 필터: 데이터를 확장, 변경, 필터링, 삭제 등의 처리를 통해 가공함
  • 출력: 데이터를 데이터 저장소(레퍼지토리)로 전송한다.

Elasticsearch 외 다양한 경로 출력이 가능하다. 따라서 Elasticsearch에서 데이터를 색인한 후에 로컬 파일이나 아마존 AWS S3 저장소로 송출도 가능하다. 

 

1-4. Kibana

Elasticsearch를 시각화하는 도구이다. aggregation의 집계 기능을 이용하여 Elasticsearch로부터 문서, 집계 결과 등을 불러와서 웹 도구로 시각화한다.

Discover, Visualize, Dashboard 3개의 기본 메뉴와 다양한 App들로 구성되어 있고, 플러그인으로 App을 설치할 수 있다.

Discover

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

Visualize

  • aggregation 집계 기능으로 조회된 데이터의 통계를 다양한 차트로 표현할 수 있는 패널을 만드는 메뉴
  • 영역차트, 바차트, 파이차트, 라인차트 등 다양한 시각화 도구를 제공한다.
  • 여기서 만들어진 패널들을 조합하여 대시보드를 만들게 된다.

Dashboard

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

 

1-5. Beats

원격 데이터 수집기.

Logstash가 데이터 수집기로서 너무 다양한 기능을 수행하여 프로그램의 부피가 커졌다. Elasticsearch 클러스터로의 대용량 데이터 전송은 다양한 시스템들로부터 수집을 하기 때문에 모든 단말 시스템에 Logstash를 설치하는 것은 큰 부담이었다. 그래서 단말 시스템으로부터 데이터를 수집하고 Elasticsearch 또는 Logstash로 데이터를 전송하는 원격 수집기가 필요했고, 이 역할을 Beats가 실행하게 되었다.

구글에서 개발된 Go 언어로 개발되었으며, 현재 Packetbeat, Libbeat, Filebeat, Metricbeat, Winlogbeat, Auditbeat 등을 개발하여 배포하고 있고 전 세계 오픈소스 개발자로부터 50여가지 이상의 Beats들이 개발되고 있다.

Packetbeat

  • 최초로 개발한 beat
  • 설치된 시스템에 유통되는 패킷들을 스니핑(snipping)하여 Elasticsearch에 적재한다.

Libbeat

  • PacketBeat에서 Elasticsearch로 전송하는 부분만을 따로 추출하여 일종의 공통 라이브러리로 만든 것
  • 특정 데이터를 수집하는 부분만 코딩하고 나면 데이터를 JSON 문서로 변환하고, 데이터가 유실되지 않게 관리하며, Elasticsearch로 전송한다.

Filebeat

  • 파일의 내용을 수집한다.
  • Web log, maching log 등 저장되는 파일 경로를 지정하기만 하면, Filebeat가 해당 경로에 적재되는 파일을 읽고 새로운 내용이 추가될 때마다 그 내용을 Elasticsearch로 색인한다.

Metricbeat

  • 실행시켜놓기만 하면 시스템에서 실행 중인 모든 프로세스들의 정보와 이 프로세스들이 소모 중인 CPU, Memory 등에 대한 상태를 수집하여 Elasticsearch에 적재한다.
  • 이를 이용하면 수집된 데이터들을 모니터링할 수 있는 시스템을 만들 수 있다.

winlogbeat

  • fMicrosoft Windows 기반 시스템에서 시스템에 적재되는 Windows event 들을 수집하여 Elasticsearch로 색인(indexing)하고, 모니터링 할 수 있다.

Auditbeat

  • 리눅스 시스템의 사용자 접속과 실행 이벤트 로그들과 같은 감사 데이터를 수집한다.
  • 주로 시스템의 보안 분석에 사용된다.

Heartbeat

  • ICMP, TCP, HTTP 프로토콜 등을 통해 Ping 명령으로 원격의 프로세스의 가동 여부를 확인하여 다른 프로세스들의 가동 시간 등을 모니터링한다.
  • 동작은 단순하지만 다양한 시스템을 동시에 모니터링 할 때 유용하다.

Functionbeat

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

 

 

2. Elasticsearch 시작하기

intro

  • 데이터 색인과 REST API 구조에 대해 알아본 후, Elasticsearch를 설치하고 실행해보자. 
  • Elasticsearch는 자바로 개발되었으므로 자바 실행이 가능한 환경이라면 어디서든 구동할 수 있다.

 

2-1. 데이터 색인

용어 정리

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

 

2-2. 설치 및 실행

(1) Elasticsearch 다운로드 설치

공식 홈페이지의 다운로드 메뉴에서 다운로드 받을 수 있다.

  • WINDOWS를 다운로드받고, 압축 해제한다.
  • Elasticsearch를 실행하려면 자바1.8 이상의 버전이 설치되어 있어야 하고, ES_JAVA_HOME 환경 변수가 잡혀있어야 한다.

 

(2) Elasticsearch 설정

Elasticsearch 설치 경로\config\elasticsearch.yml 파일을 다음과 같이 편집한다.

  • data path와 log path 지정

  • port 지정

 

(3) Elasticsearch 실행

Elasticsearch 설치 경로\bin\elasticsearch.bat 를 더블클릭하여 실행한다.

현재 [DESKTOP-JU2LQ1Q]라는 이름으로 Elasticsearch 노드가 실행된 것을 볼 수 있다. 노드 이름은 직접 지정이 가능하지만, 지정하지 않았따면 7.0 버전부터는 호스트명으로 생성된다. 5.x, 6.x 버전에서는 노드 프로세스의 UUID의 첫 7자 알파벳으로 지정된다.

 

logs 디렉터리: 실행 로그들 저장

  • 실행 중인 Elasticsearch의 실행 로그들은 logs 디렉터리 아래에 <클러스터명>.log 파일에서 확인할 수 있다.
  • 아무 설정을 하지 않았다면 기본적으로 logs/elasticsearch.log에 저장된다.

 

Elasticsearch 실행 옵션들

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

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

 

실행 옵션 1) -d: 백그라운드 실행

$ elasticsearch.bat -d​
  • -d 옵션을 추가하여 Elasticsearch를 실행하면 화면에 아무 반응 없이 명령 수행이 끝나게 된다.

windows에서 실행하니까 elasticsearch.bat을 실행시켜야 한다.

  • 백그라운드로 실행한 후 ps -ef | grep elasticsearch 명령으로 실행 중인 프로세스를 검색하면 Elasticsearch가 실행됨을 확인할 수 있다.

elasticsearch.bat가 백그라운드로 실행 중임을 확인할 수 있다.

 

elasticsearch의 logs 기록을 살펴보자.

$ head logs/elasticsearch.log

 

백그라운드 실행 중인 Elasticsearch 프로세스를 종료하려면 kill 명령을 입력해야 한다. 현재 실행 중인 프로세스를 검색했을 때 출력된 Elasitcsearch의 프로세스 ID는 2695이다.

kill 2695

kill 2695 명령어를 입력함과 동시에 실행 중인 Elasticsearch 프로세스가 종료된다.

다시 ps -ef | grep elasticsearch 를 입력하여 Elasticsearch가 실행 중인지 확인해보면, 현재 실행 중인 Elasticsearch 프로세스가 아무 것도 뜨지 않는 것을 볼 수 있다.

 

실행 옵션 2) -p: 프로세스 ID를 파일로 저장

  • -p <파일명> 옵션을 추가하여 실행된 Elasticsearch 프로세스 ID를 특정 파일에 저장할 수 있다.
// [파일명] 파일에 프로세스 ID를 저장하기
$ elasticsearch.bat -d -p [파일명]

이제 몇 가지 명령을 실행한 후에 es.pid 파일의 내용을 확인하고 실행 중인 프로세스와 비교해보자.

엥 원래 저 두 네모 박스의 값이 동일해야 하는데 나는 es.pid에 저장된 내용과 실행 중인 프로세스 ID 값이 다르다 (ㅠㅠ)

어쨌든 프로세스 ID가 저장된 es.pid 파일은 실행 중인 Elasticsearch 프로세스를 종료하게 되면 자동으로 삭제된다.

실행 중인 Elasticsearch 프로세스를 종료한 후 es.pid 파일의 내용을 보려고 하면 파일을 읽지 못한다. 프로세스 종료와 동시에 파일이 사라졌기 때문이다.

 

이제 이 옵션을 활용해서 Elasticsearch를 데몬으로 실행하는 start.sh 파일과 stop.sh 파일을 만들어보자. 각 파일의 내용은 다음과 같다.

// start.sh
bin/elasticsearch.bat -d -p es.pid
// stop.sh
kill `cat es.pid`

두 파일을 Elasticsearch 홈 경로에 저장을 한다.

// start.sh와 stop.sh 파일 생성
$ echo 'bin/selasticsearch.bat -d -p es.pid' > start.sh
$ echo 'kill `cat es.pid`' > stop.sh

두 줄의 명령어를 입력하면 각각 start.sh와 stop.sh 파일이 생성된다.

그리고 두 파일이 실행할 수 있도록 권한을 755로 변경한다.

// 실행 권한 부여하기
$ chmod 755 start.sh stop.sh

 

이제 start.sh 를 실행하여 Elasticsearch 프로세스가 실행된 것을 확인하고, 다시 stop.sh를 실행하여 Elasticsearch 프로세스를 종료해보자.

근데 es.pid에 작성된 값과 ps -ef | grep elasticsearch 명령어를 통해 Elasticsearch 프로세스 ID 값이 서로 달라서 종료가 되지 못한다(...)

 

2-3. elasticsearch 환경 설정

Elasticsearch는 노드별로 실행될 설정들을 따로 적용해서 각 노드들의 역할을 나누거나 혹은 클러스터의 속성을 결정한다.

Elasticsearch의 실행 환경을 설정하는 방법은 크게 2가지가 있는데, 1) 홈 디렉터리의 config 경로 아래에 있는 파일들을 변경하거나, 2) 시작 명령으로 설정하는 방법이다.

config 경로 아래의 파일들에서는 다음 설정들이 가능하다.

  • jvm.options : Java 힙메모리 및 환경변수
  • elasticsearch.yml : Elasticsearch 옵션
  • log4j2.properties : 로그 관련 옵션

그 외에도 Elasticsearch를 처음 실행할 때 -E 커맨드 라인 명령(4)을 통해서도 설정이 가능하다.

 

(1) jvm.options

  • Elasticsearch는 Java 가상머신 위에서 실행이 된다.
  • Elasticsearch를 실행할 때 java와 관련된 환경변수들은 대부분 jvm.options 파일에서 설정할 수 있다.
  • Elasticsearch 7.0 기준으로 1GB의 힙메모리가 기본으로 설정되어 있다. 이 설정은 jvm.options 파일 내용을 수정하여 변경할 수 있다.
// config\jvm.options
-Xms1g
-Xmx1g

 

(2) elasticsearch.yml

  • elasticsearch 실행 환경에 대한 실제 설정들을 할 수 있다.
  • YAML 문법으로 설정하기 때문에 옵션을 설정할 때 들여쓰기를 유의해서 작성해야 한다.
    • 아래 두 예시는 같은 설정을 나타낸다.
    • // config/elasticsearch.yml
      path:
           data: /var/lib/elasticsearch
           logs: /var/log/elasticsearch
    • // config/elasticsearch.yml
      path.data: /var/lib/elasticsearch
      path.logs: /var/log/elasticsearch

 ❗ 지켜야할 YAML 규칙들 

  1. 매 라인의 들여쓰기가 정확해야 한다. 그렇지 않으면 같은 레벨에 설정되어야 할 값들이 하위 레벨 설정으로 들어갈 수도 있다.
  2. <key>: <value> 가운데에 있는 콜론(:)과 <value> 값 사이에는 반드시 공백이 있어야 한다. 붙여쓰면 오류가 발생한다.

 

elasticsearch.yml에서 하는 주요 설정들

각 설정의 실제 사용 예시들은 다음장 3에서 클러스터링을 설명하는 부분에서 더 자세히 살펴보자.

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

  • 클러스터 이름을 설정한다.
  • Elasticsearch의 노드들은 클러스터 이름이 같으면 같은 클러스터로 바인딩되고, 클러스터 이름이 다르면 동일한 물리적 장비나 바인딩이 가능한 네트워크상에 있더라도 서로 다른 클러스터로 바인딩된다.
  • default 클러스터명: "elasticsearch"
  • 충돌을 방지하기 위해서 클러스터명은 반드시 고유 이름으로 설정하자.

2) node.name: "<노드명>"

  • 실행 중인 각 elasticsearch 노드를 구분할 수 있는 노드 이름을 설정한다.
  • 설정하지 않은 경우, 7.0 버전부터는 호스트명, 6.x 이하 버전에서는 프로세스 UUID의 첫 7글자가 노드명으로 설정된다.

3) node.attr.<key>: "<value>"

  • 노드별로 속성을 부여한다. (일종의 네임스페이스)
  • 이를 이용하면 예로 들어서 hot / warm 아키텍쳐를 구성할 수 있고, 물리 장비 구성에 따라 샤드 배치를 임의적으로 조절할 수도 있다.

4) path.data: ["<경로>"]

  • 인덱싱된 데이터가 저장되는 경로를 지정핞다.
  • 디폴트는 Elasticsearch가 설치된 홈 경로 아래의 data 디렉터리이다.
  • 배열 형태로 여러 개의 경로값을 입력할 수 있다. 따라서 한 서버에서 여러 개의 디스크를 사용할 수 있다.

5) path.logs: "<경로>"

  • Elasticsearch 실행 로그를 저장하는 경로를 지정한다.
  • 디폴트는 Elasticsearch가 설치된 홈 경로 아래의 logs 디렉터리이다.
  • 실행 중인 시스템 로그는 [클러스터이름].log 형태의 파일로 저장되며, 날짜가 변경되면 이전 로그 파일은 뒤에 날짜가 붙은 파일로 변경된다.

6) bootstrap.memory_lock: true

  • Elasticsearch가 사용 중인 힙 메모리 영역을 다른 자바 프로그램이 간섭하지 못하도록 미리 점유하도록 설ㅈ어한다.
  • 항상 true로 놓고 사용하는 것을 권장한다.

7) network.host: <ip주소>

  • Elasticsearch가 실행되는 서버의 ip주소
  • 디폴트는 루프백(127.0.0.1)이다.
  • 주석처리가 되어 있거나 루프백인 경우에 Elasticsearch의 노드가 개발 모드로 실행된다.
  • 만약 실제 IP 주소로 변경하게 되면 운영 모드로 실행이 되고, 노드를 시작할 때 부트스트랩을 체크한다.
  • network.hsot는 서버의 내/외부 주소를 모두 지정한다.
    • 만약 내부망에서 사용하는 주소와 외부망에서 접근하는 주소를 다르게 설정하고 싶다면 아래 값들을 이용하여구분할 수 있다.
    • network.bind_host : 내부망
    • network.publishs_host : 외부망
  • network.host 설정에 사용되는 특별한 변수값
    • _local_ : 루프백 주소(127.0.0.1). 디폴트로 설정되어 있다.
    • _site_ : 로컬 네트워크 주소로 설정된다. 실제 클러스터링 구성 시 주로 설정하는 값이다.
    • _global_ : 네트워크 외부에서 바라보는 주소로 설정한다.
  • 실제 클러스터를 구성할 때 설정을 'network.host: _site_'로 해두면 서버의 네트워크 주소가 변경되어도 설정 파일은 변경하지 않아도 되므로 편리하다.

8) http.port: <포트번호>

  • Elasticsearch가 클라이언트와 통신하기 위한 http 포트를 설정한다.
  • 디폴트는 9200이고, 포트가 이미 사용 중이라면 9200~9299 사이 값을 차례대로 사용한다.

9) transport.port: <포트번호>

  • (동일한 서버 내에 있는) Elasticsearch 노드들끼리 서로 통신하기 위한 tcp 포트를 설정한다.
  • 디폴트는 9300이고, 포트가 이미 사용 중이라면 9300~9399 사이 값을 차례대로 사용한다.

11) cluster.initial_master_nodes: ["<노드-1>", "<노드-2>"]

  • 클러스터가 최초로 실행될 떄 명시된 노드들 중에서 마스터 노드를 고른다.
  • 마스터 노드 (➡ 3.3 마스터 노드와 데이터 노드 부분 참고)

 

또한 노드 실행 시 지정된 환경 변수를 ${환경변수명} 형식으로 사용할 수 있다.

// config/elasticsearcy.yml
node.name: ${HOSTNAME}
network.host: ${ES_NETWORK_HOST}

 

(3) 노드의 역할: master, data, ingest, ml

Elasticsearch 노드들이 각자 서로 다른 역할을 수행하도록 '클러스터'를 구성한다. 아래 설정들은 모두 디폴트 값이 true이고, 기본적으로 노드는 설정에 작성된 모든 역할들을 수행한다.

특정 값을 false로 설정하여 노드의 역할을 구분짓고 클러스터를 구성하면 된다.

1) node.master: true

  • 마스터 후보(master eligible) 노드 여부를 설정한다.
  • false인 경우, 해당 노드는 마스터 노드로 선출되지 못한다.
  • 모든 클러스터는 1개의 마스터 노드가 있고, 마스터 노드가 다운되거나 끊어진 경우에는 남은 마스터 후보 노드들 중에서 새로운 마스터 노드로 선출된다.

2) node.data: true

  • 노드가 데이터를 저장하도록 설정한다.
  • false면 해당 노드는 데이터를 저장하지 않는다.

3) node.ingest: true

  • 데이터를 인덱싱할 때 전처리 작업인 ingest pipeline 작업을 수행할지 여부를 지정한다.
  • false면 해당 노드는 ingest pipeline 작업을 수행하지 않는다.

4) node.ml: true

  • 해당 노드가 머신러닝 작업을 수행하는지 여부를 지정한다.
  • false면 해당 노드는 머신러닝 작업을 수행하지 않는다.

 

예를 들어서 클러스터에서 어떤 노드가 데이터를 저장하거나 인덱싱은 하지 않고, 오직 클러스터 상태를 관리하는 마스터 노드의 역할만 수행하도록 설정하려면 다음과 같이 설정한다.

// config/elasticsearch.yml - 전용 마스터 노드 설정
node.master: true
node.data: false
node.ingest: false
node.ml: false

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

 

 ❗ 모든 값을 false로 둔다면? 

앞의 네 가지 모든 설정들을 'false'로 두면 해당 노드는 데이터를 저장하거나 인덱싱하지 않고, 클러스터 상태를 업데이트도 하지 않으며, 오직 클라이언트와 통신만 하는 역할로 사용할 수 있다. 이러한 노드를 '코디네이트 온리 노드(coordinate only node)'라고 한다.

 

(4) 커맨드 라인 설정

elasticsearch.yml 파일에 설정할 수도 있지만, Elasticsearch 실행 시 커맨드 명령에 -E [옵션]=[값] 을 작성하여 환경설정할 수도 있다.

예를 들어서 클러스터 이름은 my-cluster, 노드 이름을 node-1로 노드를 실행하려면 다음과 같이 작성하면 된다.

$ bin/elasticsearch.bat -E cluster.name=my-cluster -E node.name="node-1"

 

 

3. Elasticsearch 시스템 구조

3-1. 클러스터 구성

여러 서버를 하나의 클러스터로 실행한다면

Elasticsearch의 노드들은 총 2개의 네트워크 통신을 열어둔다.

  1. http 포트 (9200~9299) : 클라이언트와의 통신을 하기 위함
  2. tcp 포트 (9300~9399) : 노드 간에 데이터를 교환하기 위함

일반적으로 물리 서버 1개 당 하나의 노드를 실행할 것을 권장한다.

 

🔹 (ex.1) 3개의 다른 물리 서버에서 각 1개의 노드를 실행할 때 클러스터의 구성 모습

 

하나의 물리적 서버 내에서 여러 개의 노드를 실행할 수도 있다. 이런 경우에는 각 노드들이 9200, 9201, ... 순으로 차례대로 포트를 사용한다. 클라이언트는 9200, 9201 등의 포트를 통해 원하는 노드와 통신을 할 수 있다.

🔹 (ex.2) 하나의 서버에서 두 개의 노드를 실행할 때 클러스터의 구성 모습

서버1에는 두 개의 노드가 있기 떄문에 서버1의 두 번째 노드에서 실행되는 http, tcp 포트는 각각 9201, 9301이다.

물리적인 구성과 상관없이 여러 노드가 하나의 클러스터로 묶이려면 무조건 클러스터 이름(cluster.name)이 동일해야 한다. 노드들이 물리적으로 같은 서버나 네트워크망 내부에 있다고 하더라도 이 클러스터 이름이 동일하지 않으면 논리적으로 서로 다른 클러스터로 실행이 되고, 각각 별개의 시스템으로 인식된다.

 

실습: 하나의 서버에 여러 클러스터 실행하기

하나의 물리 서버에 3개의 노드를 실행시켜보자.

노드들의 이름은 node-1, node-2, node-3이고, node-1과 node-2의 클러스터명은 es-cluster-1, 그리고 node-3의 클러스터명은 es-cluster-2로 실행한다.

그러면 node-1과 node-2는 하나의 클러스터로 묶여있으므로 데이터 교환이 일어난다. node-1로 입력된 데이터는 node-2에서도 읽을 수 있고, 그 반대도 가능하다. 하지만 node-3은 클러스터가 다르기 때문에 node-1, node-2에 입력된 데이터를 node-3에서 읽을 수 없다.

이를 그림으로 나타내면 다음과 같다.

 

각 노드의 설정은 config/elasticsearch.yml에서 다음과 같이 입력하면 된다.

// node-1
cluster.name: es-cluster-1
node.name: "node-1"


// node-2
cluster.name: es-cluster-1
node.name: "node-2"


// node-3
cluster.name: es-cluster-2
node.name: "node-3"

 혹은 실행 커맨드를 입력하여 설정할 수도 있다.

$ bin/elasticsearch.bat -E cluster.name=es-cluster-1 -E node.name=node-1
$ bin/elasticsearch.bat -E cluster.name=es-cluster-1 -E node.name=node-2

$ bin/elasticsearch.bat -E cluster.name=es-cluster-2 -E node.name=node-3

 

먼저 node-1을 실행시켜보자. 노드명이 [node-1]인 것을 확인할 수 있다.

// node-1 실행
$ bin/elasticsearch.bat -E cluster.name=es-cluster-1 -E node.name=node-1

그리고 실행 메시지를 내리다보면 다음과 같은 부분이 있다.

[o.e.t.TransportService] 에서 tcp 포트 9300, 그리고 [o.e.h.AbstractHttpServerTransport] 에서 http 포트 9200을 각각 확인할 수 있다.

또한 모든 클러스터에는 반드시 하나의 마스터 노드가 존재한다. 

[o.e.c.s.MasterService] [node-1] elected-as-master 에서 현재 node-1이 마스터 노드로 선출된 것을 확인할 수 있다. 

 

이제 node-1이 실행 중인 상태에서 node-2를 실행시키자.

// node-2 실행
$ bin/elasticsearch.bat -E cluster.name=es-cluster-1 -E node.name=node-2

하지만 에러가 발생하여 실행이 되지 않는다.

오류를 구글링해보니 이미 node-1로 인해서 elasticsearch가 실행 중이라, 저 명령어를 입력해서 node-2를 실행하기 위해 elasticsearch를 중복 실행할 순 없다는 것이다(...)

+
질문을 하니, 예로 들어서 3000번 포트로 서버를 실행 중인 경우에 또 다시 3000번 포트로 다른 서버를 실행시키면 이미 3000번 포트로 서버가 열려 있어서 실행이 되지 않고 다른 포트인 3001번으로 서버를 실행시킬래요? 하고 물어본다. 이처럼 elasticsearch도 이미 실행 중인 경우에는 다른 터미널에서 다시 실행 시킬 수가 없다는 것이다.
해당 가이드북에서는 어떻게 실행한 것인지는 모르겠지만(...) 실행이 되지 않으니 어쩔 수 없이 실습은 스킵하고, 개념만 제대로 이해하고 넘어가기로 하였다. 

 

 

어쨌든 만약 node-2의 실행을 성공하게 되면, 이론적으로 공부했던 것처럼 tcp 포트는 9301번, http 포트는 9201번으로 잡히는 것을 볼 수 있다.

...
[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}
...

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

node-2를 실행한 후 다시 node-1의 콘솔 메시지를 확인해보면 node-2가 동일한 클러스터에 추가되었다는 메시지를 확인할 수 있다.

// node-1 실행 화면
[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}

 

 

이제 node-1, node-2가 실행 중인 상태에서 node-3을 추가로 실행해보자.

// node-3 실행
$ bin/elasticsearch.bat -E cluster.name=es-cluster-2 -E node.name=node-3

그리고 출력을 확인해보자.

// 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}
  • 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 클러스터의 마스터 노드로 선출된 것을 확인할 수 있다.

 

디스커버리 (Discovery)

노드가 처음 실행이 될 때 같은 서버 내에 있는 다른 노드들, 혹은 discovery.seed_hosts: [ ... ] 에 설정된 네트워크 상의 다른 노드들을 찾아서 하나의 클러스터로 바인딩하는데, 이 과정을 '디스커버리'라고 한다.

 

elasticsearch.yml에서 작성하는 discovery 설정하기 내용 다시 살펴보기

10) discovery.seed_hosts: ["<호스트-1>", "<호스트-2>", ...]

  • 클러스터를 구성할 때 바인딩할 원격 노드의 IP 혹은 도메인 주소를 배열 형태로 입력한다.
  • 주소만 적는 경우 디폴트로 9300~9305 사이 포트 값을 검색하고, tcp 포트가 이 범위 밖에 설정된 경우 포트번호까지 함께 적어야 한다. (노드 간에 데이터 교환을 하기 위해선 tcp 포트를 사용하기 때문에 노드를 찾기 위해서 tcp 포트를 검색함)
  • 이처럼 원격에 있는 노드들을 찾아 바인딩하는 과정을 '디스커버리'라고 한다.

 

디스커버리 순서

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

 

❗ 클러스터에 노드가 무수히 많다해도 보통 discovery_seed_hosts 설정에는 처음에 탐색할 노드 3~5개 정도만 설정하면 큰 문제 없이 클러스터가 바인딩된다. 보통 마스터 후보 노드들을 지정하고, 처음 탐색하는 대상 노드는 반드시 가동 중인 상태여야만 한다.

 

 

3-2. 인덱스(Index)와 샤드(Shards)

인덱스, 도큐먼트, 샤드

Elasticsearch에서 단일 데이터 단위를 도큐먼트(document)라 하고, 이 도큐먼트들을 모아놓은 집합을 인덱스(index)라 한다. 인덱스라는 단어는 여러 뜻으로도 사용되기 때문에 데이터 저장 단위인 인덱스는 인디시즈(indices)라 표현하기도 한다. 이 가이드북에서는 데이터를 Elasticsearch에 저장하는 행위인덱싱, 그리고 도큐먼트의 집합 단위인덱스라고 부를 것이다.

인덱스는 기본적으로 샤드(shard)라는 단위로 쪼개지며, 각 노드에 분산되어 저장된다. 샤드는 루씬의 단일 검색 인스턴스인데, 이렇게 도큐먼트를 샤드라는 단위로 쪼개서 저장하면 더 빠르게 검색할 수 있다. 왜냐하면 샤드는 병렬적으로 실행되기 때문이다. 도큐먼트 하나에 기본으로 설정되는 샤드 수는 1개이며, 인덱스 단위로 샤드 수를 설정할 수 있다.

다음은 하나의 인덱스가 5개의 샤드로 저장되도록 설정한 예시이다.

 

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

  • 인덱스를 생성할 때 별도의 설정을 하지 않으면 Elasticsearch 7.0 버전부터는 디폴트로 1개의 샤드로 인덱스가 구성되고, 6.x 이하 버전에서는 5개로 구성된다.
  • 클러스터에 노드를 추가하면 샤드들이 각 노드들로 분산되고, 디폴트로 1개의 복제본을 생성한다.
  • 처음 생성된 샤드를 프라이머리 샤드(Primary Shard), 복제본을 리플리카(Replica)라 한다.

예제

예를 들어서 Node-1에 있는 하나의 인덱스에 5개의 샤드로 구성되도록 설정하였고, 클러스터가 4개의 노드로 구성되어 있다고 하자. 그러면 Node-1의 샤드와 리플리카들은 어디에 위치해있을까?

우선 Node-1의 샤드로는 5개의 프라이머리 샤드가 있을 것이고, 각 프라이머리 샤드의 복제본인 리플리카가 1개씩 있을 것이다. 따라서 총 리플리카 수는 샤드5개x1개 이므로 5개이다. (프라이머리 샤드 5개) + (리플리카 5개) = 총 샤드 10개. 이 전체 샤드들은 클러스터 내 존재하는 전체 노드에 골고루 분배되어 저장된다.

5개의 프라이머리 샤드와 복제본이 4개의 노드에 분산되어 저장된 예

 

 ❗ 주의할 점 

  • 만약 노드가 1개 있는 경우에는 프라이머리 샤드만 존재하고 복제본은 생성되지 않는다.
  • Elasticsearch는 아무리 작은 클러스터라도 데이터 가용성과 무결성을 위해 최소 3개의 노드로 구성할 것을 권장한다.

 

프라이머리 샤드와 이의 복제본인 리플리카는 서로 동일한 데이터를 담고 있고, 반드시 서로 다른 노드에 저장된다.

만약 위의 그림에서 Node-3 노드가 시스템 다운이나 네트워크 단절 등의 문제로 연결이 끊기거나 사라지게 된다면 이 클러스터는 Node-3에 있던 0번과 4번 프라이머 샤드를 유실하게 된다. 하지만 아직 다른 노드들(Node-1, Node-2)에 0번, 4번 샤드가 남아있기 때문에 여전히 전체 데이터는 유실 없이 사용할 수 있게 된다. 샤드가 유실되면 다른 노드에 또다시 리플리카를 생성한다.

Node-3 노드가 유실되었을 때 0번, 4번 샤드를 다른 노드에 리플리카를 다시 새로 생성한 예

처음에 클러스터는 먼저 유실된 노드가 복구되기를 기다린다. 하지만 타임아웃이 되면 Elasticsearch는 유실된 노드가 더이상 복구되지 않는다고 판단한다. 리플리카가 사라져 1개만 남은 0번과 4번 프라이머리 샤드를 다른 노드에 다시 새로 복제한다. 그러면 처음에 4개였던 노드가 3개로 줄어들어도 0~4번까지의 프라이머리 샤드, 리플리카가 각각 5개씩, 총 10개의 데이터로 유지된다.

노드가 3개로 줄어들었을 때에도 전체 데이터가 유지됨

 

이렇게 프라이머리 샤드와 리플리카 덕분에 Elasticsearch는 운영 중에 노드가 유실되어도 데이터를 잃어버리지 않고 데이터의 가용성과 무결성을 보장할 수 있다.

 

 ❗ 프라이머리 샤드가 유실된 경우에는 새로운 프라이머리 샤드가 생성되는 것이 아니라, 남아있던 리플리카가 프라이머리 샤드로 승격이 되고 다른 노드에 새로운 리플리카를 생성한다. 

 

샤드 개수 설정

샤드(프라이머리 샤드와 리플리카)의 개수는 인덱스를 처음 생성할 때 설정할 수 있다. 하지만 프라이머리 샤드의 개수는 인덱스를 재인덱싱하지 않는 이상 바꿀 수 없고, 리플리카의 개수는 언제든 변경할 수 있다.

다음은 curl 명령으로 REST API로 프라이머리 샤드는 4개, 리플리카는 1개인 'books'라는 이름의 인덱스를 생성하는 예제이다.

// 프라이머리 샤드 5, 복제본 1인 books 인덱스 생성하기
$ curl -XPUT "http://localhost:9200/books" -H 'Content-Type: application/json' -d'
{
  "settings": {
    "number_of_shards": 5,
    "number_of_replicas": 1
  }
}'

다음 명령어를 입력하면 books 인덱스의 리플리카 수를 0으로 변경할 수 있다.

// books 인덱스의 리플리카 개수를 0으로 변경하기
$ curl -XPUT "http://localhost:9200/books/_settings" -H 'Content-Type: application/json' -d'
{
  "number_of_replicas": 0
}'

 

만약 4개의 노드를 가진 클러스터에 프라이머리 샤드 5개, 리플리카 1개인 books 인덱스, 그리고 프라이머 샤드 3개와 리플리카 0개인 magazines 인덱스가 있다면, 전체 샤드들은 다음과 같은 모양으로 배치될 수 있다.

books 인덱스와 magazines 인덱스

 

 

3-3. 마스터 노드(Master Nodes)와 데이터 노드(Data Nodes)

마스터 노드 (Master Node)

마스터 노드란?

  • Elasticsearch의 클러스터는 하나 이상의 노드들로 이뤄진다. 이 중에서 하나의 노드는 인덱스의 메타 데이터나 샤드의 위치와 같이 클러스터 상태(Cluster Status) 정보를 관리하는 마스터 노드의 역할을 수행한다.
  • 클러스터마다 하나의 마스터 노드가 있으며, 마스터 노드의 역할을 수행하는 노드가 없다면 클러스터는 작동이 정지된다.
  • elasticsearch.yml에서 마스터 노드의 디폴트 설정은 node.master: true이다.

 

마스터 후보 노드

  • 기본적으로는 모든 노드가 마스터 노드로 선출될 수 있는 마스터 후보 노드(master eligible node)이다. 
  • 만약 현재 마스터 역할을 수행하는 노드가 네트워크상에서 끊어지거나 다운되면 다른 마스터 후보 노드 중 하나가 마스터 노드로 선출되어 대신 마스터 노드의 역할을 수행한다.
  • 마스터 후보 노드들은 처음부터 마스터 노드의 정보들을 공유하고 있기 때문에 바.로. 즉.시. 마스터 노드의 역할을 수행할 수 있다.
  • 클러스터가 커져서 노드와 샤드의 개수가 많아지게 되면 모든 노드들이 마스터 노드의 정보를 계속 공유하는 것은 부담이 된다. 이런 경우에는 마스터 노드의 역할을 수행할 마스터 후보 노드를 따로 설정하면 전체 클러스터 성능이 향상된다.
  • 마스터 노드로 사용하지 않을 노드들은 미리 설정값을 node.master: false로 설정하자.

 

데이터 노드 (Data Node)

  • 실제로 인덱싱된 데이터를 저장하고 있는 노드
  • 클러스터에서 마스터 노드와 데이터 노드를 분리하여 설정할 때, 마스터 후보 노드들node.data: false로 설정하여 마스터 노드 역할만 하고 데이터는 저장하지 않도록 한다.
  • 이렇게 하면 마스터 노드데이터를 저장하지 않고 오로지 클러스터만 관리하게 되고, 데이터 노드클러스터 관리 작업을 하지 않고 데이터 처리에만 집중할 수 있다.

 

예제

설정 조건

  • 클러스터 내에 총 4개의 노드가 있다.
  • node-1은 마스터의 역할만 실행하는 전용 노드(Dedicated Master Node)
  • node-2, node-3, node-4는 마스터 역할을 하지 않고 오로지 데이터 저장만 하는 노드

노드 생성하기

// config/elasticsearcy.yml

// Node-1 (마스터 전용 노드)
node.master: true
node.data: false

// Node-2 (데이터 노드)
node.master: false
node.data: true

// Node-3 (데이터 노드)
node.master: false
node.data: true

// Node-4 (데이터 노드)
node.master: false
node.data: true

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

마스터 전용 노드와 데이터 노드의 구분

이는 Kibana의 Monitoring 화면에서 노드의 역할들을 확인할 수 있다. Elastic Stack 모니터링 도구에서 마스터 노드는 별(*)로 표시된다.

Kibana 모니터링 화면에서 마스터 노드와 데이터 노드 확인하기

 

 ❗ 실제 환경에서는 위의 예제처럼 마스터 후보 노드를 1개만 설정해서 안되고, 최소 3개 이상의 홀수개로 설정해야 한다. (그 이유는 Split Brain 문제에서 살펴보자!)

 

Split Brain

마스터 후보 노드를 하나만 두면 그 마스터 노드가 유실되었을 때 클러스터 전체가 작동이 정지될 위험이 있다. 그래서 최소한 백업용 마스터 노드를 설정하는데(샤드의 복제본인 리플리카처럼), 이 때 마스터 후보 노드들은 3개 이상의 홀수 개로 두는 것을 권장한다. 만약에 마스터 후보 노드를 2개 혹은 짝수로 운영하는 경우에는 네트워크 유실로 인해 다음과 같은 상황을 겪을 수 있기 때문이다.

네트워크 단절로 인한 클러스터 분리 문제 발생

위 그림과 같이 네트워크가 단절되어 마스터 후보 노드인 node-1과 node-2가 분리되면 각자가 서로 다른 클러스터로 구성되어 계속 동작하게 될 수도 있다. 이 상태에서 각자 클러스터에 데이터가 추가되거나 변경된 후에 나중에 네트워크가 복구되고 하나의 클러스터로 다시 합쳐지면 데이터 정합성에 문제가 생기게 되고, 그러면 데이터 무결성을 유지할 수 없게 된다. 이러한 문제를 Split Brain이라 한다.

Split Brain이 일어나지 않기 위해서는 마스터 후보 노드를 3개 두고, 클러스터에 마스터 후보 노드가 최소 2개 이상 존재할 때에만 클러스터가 동작하도록하고, 그렇지 않으면 클러스터가 동작을 멈추도록 해야 한다.

6.x 이전 버전에서는 elasticsearch.yml에서 discovery.zen.minimum_master_nodes 설정으로 지정할 수 있다.

// elasticsearch.yml
discovery.zen.minimum_master_nodes: 2

minimun_master_nodes의 값은  (전체 마스터 후보 노드)/2+1 개로 설정해야 한다. 따라서 마스터 후보 노드가 5개인 경우에는 3으로 설정한다.

7.0부터는 discovery.zen.minimun_master_nodes 설정이 사라지고 대신에 node.master: true인 노드가 있으면 클러스터가 스스로 minimun_master_nodes 값을 변경해준다. 따라서 사용자는 최초 마스터 후보로 선출할 cluster.initial_master_nodes: [ ]  값만 설정하면 된다.

이렇게 설정하고 나면 네트워크가 단절되었을 때 클러스터에서 minimun_master_nodes를 2이상으로 설정했다면 살아있고 그렇지 않은 경우에는 클러스터 동작을 멈춘다.

클러스터 분리 시 마스터 노드가 절반 이상인 클러스터만 생존한다.

위 그림은 node-1, node-2, node-3 총 3개를 마스터 후보 노드로 둔 경우이다. 여기서 갑자기 네트워크가 단절되어 node-2가 다른 마스터 노드들과 끊어진다고 하자.

node-2가 끊어졌으므로 클러스터가 두 개로 분리되는데, node-1, node-3 두 노드를 가진 클러스터를 클러스터1, node-2를 가진 클러스터를 클러스터2라 하자. 그러면 현재 클러스터1에는 마스터 노드를 2개, 클러스터2에는 마스터 노드를 1개 가지고 있다. 총 마스터 노드 수인 3개에서 절반은 1.5개. 따라서 클러스터1와 클러스터2 중에서 1.5개 이상의 마스터 노드를 가진 클러스터인 클러스터1은 계속 동작하고, 1.5개 이하의 마스터 노드를 가진 클러스터인 클러스터2는 동작을 멈춘다.

그러면 데이터는 node-4, node-5의 데이터만 사용할 수 있고, node-6은 네트워크가 복구될 때까지 동작하지 않고 노드가 분리되기 이전 상태 그대로 유지된다. 이렇게 하면 나중에 클러스터가 복구되었을 때 node-4, node-5에 추가되거나 변경, 삭제된 데이터의 정보들이 node-6에 업데이트되므로 데이터 정합성에 문제가 없게 된다.

따라서 마스터 후보 노드 개수를 홀수개로 두는 이유는 네트워크가 단절되었을 때 분리된 두 클러스터 중에서 계속 동작할 클러스터와 동작을 멈출 클러스터를 정하기 위해서다.

 

이처럼 Split Brain 문제를 피하려면 마스터 후보 노드 개수를 항상 홀수개로 두고, 최소 마스터 후보 노드는 (전체 마스터 후보 노드)/2+1로 설정해야 한다.

 

 

4. Elasticsearch 데이터 처리

intro

  • Elasticsearch는 데이터 저장 형식으로 json 문서 형식을 사용한다.
  • 데이터 저장 형식 뿐 아니라, 쿼리와 클러스터 설정 등 모든 정보를 json 형태로 주고 받음

 

4-1. REST API

RESTful API

Elasticsearch는 http 프로토콜로 접근할 수 있는 REST API를 지원한다. URL로 접근할 수 있고, http 메서드인 PUT, POST, GET, DELETE로 자원을 처리할 수 있다. 이러한 특성을 가진 시스템을 RESTful 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/delete.jsp?name=kim
  • 조회 : http://user.com/get.jsp?name=kim

하지만 REST API를 지원하는 시스템은 kim이라는 사용자에 대해서 PUT, GET, DELETE 같은 http 메서드로 하나의 URL에만 접근하여 데이터를 처리할 수 있다.

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

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

 

유닉스 curl

MacOS, 리눅스와 같은 유닉스 기반 운영체제에서는 curl 명령어로 간편하게 REST API를 사용할 수 있다. Elasticsearch를 실행하고 curl 명령을 이용해서 elasticsearch 클러스터의 최상위 경로를 호출하면 아래처럼 클러스터 상태 정보가 json 형식으로 출력된다.

// GET 메서드로 elasticsearch 클러스터 조회하기
$ 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"
}

리턴된 결과로 노드명, 클러스터명, Elasticsearch 버전, 루씬 버전 등 정보들을 담고 있다.

 

Kibana Dev Tools

REST API를 쉽게 사용하려면 Postman 같은 도구를 사용할 수 있다. Kibana에는 elasticsearch엥서 REST API를 간편하게 실행할 수 있는 Dev Tools를 제공한다.

먼저 Kibana를 실행하려면 Elastic 홈페이지에서 운영체제별로 맞는 Kibana 버전을 내려받고 압축을 푼다. 

홈페이지에서 Kibana 다운로드 받기

 

그리고 bin/kibana.bat를 실행시키면, 실행 중인 elasticsearch와 동일한 호스트의 통신 포트 9200인 localhost:9200에서 elasticsearch와 통신하며 실행된다.

만약 Elasticsearch와 Kibana가 서로 다른 호스트에서 실행되고 있거나 통신 포트가 9200이 아니라면 Kibana 홈 config 디렉터리 아래에 있는 kibana.yml 파일에서 elasticsearch.url: "http://localhost:9200" 옵션으로 설정하면 된다.

bin/kibana.bat를 실행시켰을 때

기본적으로 Kibana는 포트 5601번에서 실행이 된다. 만약 변경하고 싶다면 server.port: 5601을 변경하고 싶은 포트 값으로 설정하면 된다.

localhost:5601 접속했을 때

kibana.bat을 열어 Kibana를 실행한 뒤 localhost:5601로 접속하면 KIbana를 바로 사용할 수 있다. Kibana Dev Tools는 쿼리 자동 완성도 되고, 호스트 경로도 별도로 입력할 필요가 없다. 그리고 Dev Tools에 입력한 명령을 curl 명령으로 변환하여 클립보드에 복사할 수도 있다.

Kibana의 Dev Tools 메뉴

가이드북 페이지에서는 위의 위치에 있다고 했으나, 현재는 아래와 같은 위치에 있었다. (메뉴에서 맨 아래로 내려야 함)

Dev Tools 화면

 

 

4-2. CRUD - 입력, 조회, 수정, 삭제

도큐먼트 URL

Elasticsearch는 하나의 도큐먼트별로 고유한 URL를 갖는다. 도큐먼트에 접근하는 URL은

http://호스트:포트/인덱스/_doc/도큐먼트id

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

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

구조였지만, Elasticsearch 7.0부터는 도큐먼트 타입 개념이 사라지면서 대신 고정자 _doc로 접근한다.

 

예제

다음은 curl 도구를 사용하여 my_index 인덱스에 도큐먼트 id가 1인 데이터를 입력하는 예제이다.

// my_index/_doc/1 에 도큐먼트 입력하기
$ 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}

 

이후부터 elasticsearch의 REST 명령들은 Kibana의 Dev Tools에서 입력하는 형식으로 설명될 것이다. 입력은 request, 응답은 response로 표현할 것이다.

 

1) 입력(PUT)

  • 데이터를 입력할 때 PUT 메서드를 사용한다.

 

처음으로 도큐먼트 생성하기

다음은 Kibana에서 my_index 인덱스에 도큐먼트 id가 1인 데이터를 입력하는 예제이다.

request

// my_index/_doc/1 최초 입력
PUT my_index/_doc/1
{
  "name":"Jongmin Kim",
  "message":"안녕하세요 Elasticsearch"
}

response

// my_index/_doc/1 최초 입력 결과
{
  "_index" : "my_index",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}

처음으로 도큐먼트를 생성하면 결과에 "result": "created" 가 표시된다. 동일한 URL에 다른 내용의 도큐먼트를 입력하면 기존의 도큐먼트는 삭제되고 새로운 도큐먼트로 덮어쓴다. 이때 결과에 created가 아닌 updated가 표시된다.

 

도큐먼트 재입력하기 (처음으로 도큐먼트 생성X)

request

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

response

// my_index/_doc/1 재입력 결과
{
  "_index" : "my_index",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 2,
  "result" : "updated",
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "failed" : 0
  },
  "_seq_no" : 1,
  "_primary_term" : 1
}

 

_create

실수로 도큐먼트를 재입력하여 기존의 도큐먼트가 덮어씌워질 수도 있다. 이를 방지하기 위해서 입력 명령에 _doc 대신 _create를 사용하면 도큐먼트를 재입력했을 때 덮어씌워지지 않는다. 만약 입력하는 도큐먼트 id에 이미 데이터가 있다면 다음 오류가 발생한다. 

request

➡ 6.x 이전 버전에서는 PUT 인덱스/도큐먼트 타입/도큐먼트id/_create 형식으로 작성한다.

// _doc 대신 _create로 새 도큐먼트 입력하기
PUT my_index/_create/1
{
  "name":"Jongmin Kim",
  "message":"안녕하세요 Elasticsearch"
}

response

// 이미 도큐먼트가 있는 경우에는 _create 명령 실행 불가함
{
  "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
}

 

2) 조회(GET)

GET 메서드로 가져올 도큐먼트 URL을 입력하면 도큐먼트의 내용을 가져온다. 다양한 정보가 함께 표시되고, 문서의 내용은 _source 항목에 나타난다.

예제

request

// my_index/_doc/1 도큐먼트 조회 (my_index 인덱스명의 도큐먼트 id가 1인 데이터 가져오기)
GET my_index/_doc/1

response

// my_index/_doc/1 도큐먼트 조회 결과
{
  "_index" : "my_index",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 2,
  "_seq_no" : 1,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "name" : "Jongmin Kim",
    "message" : "안녕하세요 Elasticsearch"
  }
}

 

 

3) 삭제(DELETE)

DELETE 메서드로 도큐먼트 혹은 인덱스를 삭제할 수 있다. 두 경우는 차이가 있는데, 먼저 도큐먼트를 살펴보자.

 

도큐먼트 삭제하기

DELETE my_index/_doc/1 명령어로 하나의 도큐먼트를 삭제하면 다음과 같이 도큐먼트가 삭제되었다고 "result": "deleted" 결과가 리턴된다.

request

// my_index/_doc/1 삭제
DELETE my_idnex/_doc/1

response

// my_index/_doc/1 도큐먼트 삭제 결과
{
  "_index" : "my_index",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 3,
  "result" : "deleted",
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "failed" : 0
  },
  "_seq_no" : 2,
  "_primary_term" : 1
}

 

삭제된 도큐먼트 확인하기

도큐먼트는 삭제되었지만 인덱스는 남아있는 경우에 삭제된 도큐먼트를 GET해서 가져오면 다음과 같이 my_index/_doc/1 도큐먼트를 찾지 못했다는 "found": false 응답을 받는다.

request

// 삭제된 my_index/_doc/1 도큐먼트 조회하기
GET my_index/_doc/1

response

// my_index/_doc/1 도큐먼트 조회 결과
{
  "_index" : "my_index",
  "_type" : "_doc",
  "_id" : "1",
  "found" : false
}

 

인덱스 삭제하기

이제 인덱스 전체를 삭제해보자. 그러면 다음과 같이 "acknowledged": true 응답만 리턴된다.

request

// my_index 인덱스 전체 삭제하기
DELETE my_index

response

// my_index 인덱스 전체 삭제 결과
{
  "acknowledged" : true
}

 

삭제된 인덱스나 혹은 처음부터 없었던 인덱스의 도큐먼트를 조회하면 도큐먼트를 찾지 못했다는 "found": false 응답이 아니라, 인덱스를 찾지 못했다는 "type": "index_not_found_exception", "status": 404 라는 오류가 리턴된다.

request

// 삭제된 my_index 인덱스 도큐먼트 조회하기
GET my_index/_doc/1

response

// 삭제된 my_index 인덱스의 조회 결과
{
  "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
}

 

4-1) 수정(POST)

POST 메서드는 PUT 메서드와 유사하게 데이터를 입력하는데 사용된다. 도큐먼트를 입력할 때 POST 메서드로 <인덱스>/_doc 까지만 입력하면 자동으로 해당 인덱스에 임의의 도큐먼트id가 생성된다. 이 자동생성 기능은 PUT 메서드로는 동작하지 않는다.

request

// POST 명령으로 my_index/_doc 도큐먼트 입력
POST my_index/_doc
{
  "name":"Jongmin Kim",
  "message":"안녕하세요 Elasticsearch"
}

response

// POST 명령으로 도큐먼트 입력 결과 - 도큐먼트 아이디 자동 생성됨
{
  "_index" : "my_index",
  "_type" : "_doc",
  "_id" : "ZuFv12wBspWtEG13dOut",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}

해당 예제에서는 도큐먼트 id "_id" : "ZuFv12wBspWtEG13dOut" 가 자동 생성 된 것을 확인할 수 있다.

 

4-2) _update: 하나의 필드만 내용 수정하기

기존 도큐먼트의 URL에 변경될 내용의 도큐먼트 내용을 다시 PUT 하면 기존 도큐먼트의 내용을 수정할 수 있다. 하지만 여러 필드가 있는 도큐먼트에서 필드 하나만 바꾸려고 전체 도큐먼트 내용을 매번 다시 입력하는 건 번거롭다. 이런 경우에는 POST 인덱스/_update/도큐먼트id 명령어를 작성하여 원하는 필드의 내용만 업데이트할 수 있다. 그리고 업데이트 할 내용에 "doc"라는 지정자를 사용한다.

 

예제

_update API를 사용해서 my_index/_doc/1 도큐먼트의 "message" 필드 값을 "안녕하세요 Kibana"로 변경해보자. 이전에 my_index/_doc/1 도큐먼트를 삭제하였으므로 다시 PUT 메서드로 도큐먼트를 생성하고 업데이트 명령어를 수행해야 한다.

request

// my_index/_update/1 도큐먼트의 message 필드만 업데이트하기
POST my_index/_update/1
{
   "doc": {
         "message": "안녕하세요 Kibana"
   }
}

response

// my_index/_update/1 도큐먼트의 message 필드 수정한 결과
{
  "_index" : "my_index",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 2,
  "result" : "updated",
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "failed" : 0
  },
  "_seq_no" : 1,
  "_primary_term" : 1
}

 

이제 다시 GET 명령으로 my_index/_doc/1 도큐먼트를 조회해보자. message 필드가 변경된 것을 확인할 수 있다.

또한 결과를 보면 "_version": 2 로 버전이 증가한 것도 확인할 수 있다. _update API를 사용해서 하나의 필드만 수정하면, 실제 내부에서는 (1) 도큐먼트 전체 내용을 가져오고 (2) _doc에서 지정된 필드(message)의 내용(안녕하세요 Kibana)을 변경한 새 도큐먼트를 만든 후에 (3) 전체 내용을 다시 PUT으로 입력하는 과정으로 수정하기 때문이다.

 

4-3. 벌크 API (_bulk API)

_bulk API란?

  • 여러 명령어를 실행시킬 수 있다.
    • 불필요한 오버헤드가 없으므로 명령어를 따로 수행하는 것보다 속도가 훨씬 빠르다.
  • index, create, update, delete 가 가능하다.
  • delete는 내용을 입력할 필요가 없으므로 명령문만 작성하고, delete를 제외하고는 명령문과 데이터문을 한 줄씩 순서대로 입력한다.
  • Logstash와 Beats, Elastic 웹페이지에서 제공하는 대부분의 클라이언트에서는 데이터를 입력할 때 _bulk를 사용할 수 있도록 개발되어 있다.

 

 ❗ _bulk 명령문과 데이터문은 반드시 한 줄 안에 입력이 되어야 한다. 줄바꿈은 허용되지 않는다! 

 ❗ Elasticsearch에는 커밋이나 롤백 등의 트랜잭션 개념이 없다. 따라서 _bulk를 작업하다가 중간에 연결이 끊어지거나 시스템이 다운되어 동작이 중단된 경우에는 어디까지 동작이 실행되었는지 확인할 수가 없다. 따라서 보통 이런 경우에는 전체 인덱스를 삭제하고 처음부터 다시 하는 것이 안전하다. 

 

예제

각 명령의 결과가 items에 배열로 리턴된다.

🔹 코드 먼저 살펴보기

request

// _bulk 명령 실행
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"}}

response

// _bulk 명령 실행 결과
{
  "took" : 470,
  "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
      }
    },
    {
      "delete" : {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "2",
        "_version" : 2,
        "result" : "deleted",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 2,
        "_primary_term" : 1,
        "status" : 200
      }
    },
    {
      "create" : {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "3",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 3,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "update" : {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "1",
        "_version" : 2,
        "result" : "updated",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 4,
        "_primary_term" : 1,
        "status" : 200
      }
    }
  ]
}

 

🔹 코드 뜯어보기

모든 명령어가 동일한 인덱스에서 수행된다면 다음과 같이 인덱스명/_bulk 형식으로 작성하면 된다.

 

벌크 명령어를 파일에 저장하고 실행하기

벌크 명령을 파일로 저장하고 curl 명령으로 실행할 수도 있다. 저장한 벌크 명령 파일을 --data-binary로 지정하면 된다. 

예제

다음 내용을 bulk.json이라는 파일로 저장해보자.

// 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"}}

그리고 다음 명령을 작성하여 bulk.json 파일에 있는 모든 내용을 _bulk 명령으로 실행하자. 파일 이름 앞에는 @ 문자입력한다.

// bulk.json 파일 내용을 벌크 명령어로 실행하기
$ curl -XPOST "http://localhost:9200/_bulk" -H 'Content-Type: application/json' --data-binary @bulk.json

 

 

4-4. 검색 API (search API)

Elasticsearch 쿼리 검색

Elasticsearch의 진가는 쿼리를 통한 검색 기능에 있다!

  • 인덱스 단위로 검색할 수 있다.
  • GET 인덱스명/_search 형식으로 사용한다.
  • 쿼리를 입력하지 않으면 전체 도큐먼트를 찾는 match_all 검색을 한다.

 

URI 검색

URI 검색이란?

_search 뒤에 q 파라미터를 작성하여 검색어를 입력할 수 있다. 이처럼 요청 주소에 검색어를 넣어서 검색하는 방식을 URI 검색이라 한다.

 

예제1: 한 개의 검색어로 검색하기

앞에서 벌크 명령으로 만든 test 인덱스에서 "value"라는 값을 찾고 싶다면 다음과 같이 입력한다.

request

// URI 검색으로 검색어 "value" 검색하기
GET test/_search?q=value

response

// URI 검색으로 "value" 검색한 결과
{
  "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"
        }
      }
    ]
  }
}

  • hits.total.value - 검색 결과 찾은 도큐먼트의 개수
  • hits: [... ] - 가장 정확도가 높은 문서 10개가 배열로 나타남
    • 이 정확도를 relevancy라 한다.

 

예제2: 두 개 이상의 검색어로 검색하기 (AND, OR, NOT)

  • "value"와 "three" 두 개 검색어로 검색하려면 AND 조건으로 작성한다.
  • URI 쿼리에서는 AND, OR, NOT 을 사용할 수 있고 반드시 대문자로 입력해야 한다.

request

// URI 검색으로 검색어 "value AND three" 검색하기
GET test/_search?q=value AND three

response

// URI 검색으로 검색어 "value AND three" 검색 결과
{
  "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"
        }
      }
    ]
  }
}

value와 three를 모두 포함한 test/_doc/3 도큐먼트 만이 나온다.

 

예제3: 특정 필드에서 검색어 검색하기

검색어 value를 field 필드에서 찾고 싶다면 다음과 같이 필드명: 검색어 형태로 입력하면 된다. 검색은 항상 필드를 지정하는게 좋다.

request

// URI 검색으로 "field" 필드에서 검색어 "value" 검색하기
GET test/_saerch?q=field:value

response

{
  "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"
        }
      }
    ]
  }
}

 

  • URI 검색은 루씬의 기본 쿼리 문법을 사용하여 더 쉽게 작성할 수도 있다.
  • 또한 웹 브라우저 주소창 등에서도 사용할 수 있어서 빠르고 쉽게 쓸 순 있지만, 더 복잡한 검색을 하려면 데이터 본문(data body) 검색을 해야 한다.

 

데이터 본문(Data Body) 검색

데이터 본문 검색이란?

  • 검색 쿼리를 데이터 본문(data body)으로 입력하는 방식
  • Elasticsearch QueryDSL을 사용한다.
  • 쿼리는 JSON 형식으로 되어 있다.
  • 쿼리를 입력할 땐 항상 query 지정자로 시작한다.

 

예제: match 쿼리

  • match 쿼리는 가장 쉽고 많이 사용되는 쿼리이다.
  • 필드명이 field인 필드의 값이 value인 도큐먼트 검색하기
    • 쿼리 입력은 항상 query로 시작한다.
    • 다음 쿼리의 종류를 지정한다. 해당 예제의 경우에는 match 쿼리를 지정한다.
    • 다음 사용할 쿼리문은 쿼리별로 다르게 작성될 수 있는데,match 쿼리 필드명:검색어 방식으로 입력한다.

request

// 데이터 본문 검색으로 "field" 필드에서 검색어 "value"를 검색하기
GET test/_search
{
   "query": {
      "match": {
         "field": "value"
      }
   }
}

response

// 데이터 본문 검색으로 "field" 필드에서 검색어 "value" 를 검색한 결과
{
  "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"
        }
      }
    ]
  }
}

 

멀티테넌시(Multitenancy)

멀티테넌시란?

  • 여러 개의 인덱스를 한꺼번에 묶어서 검색할 수 있다.
    • logs-2021-01, logs-2021-02 ... 와 같이 날짜별로 저장된 인덱스들이 있다면, 인덱스들은 모두 logs-*/_search 명령으로 한꺼번에 검색할 수 있다.
  • 특히 시간순으로 쌓이는 로그 데이터를 다룰 때 인덱스를 일단위 등으로 구분하면 좋다.
  • 나중에 필드 구조가 변경되거나 크기가 커져서 샤드 설정을 변경할 때 용이하다.
  • 여러 인덱스를 검색할 땐 쉼표(,)로 나열하거나 와일드카드(*) 문자로 묶을 수 있다.

 

예제1: 쉼표로 나열해서 여러 인덱스 검색하기

GET logs-2021--01,2021-02,2021-03/_search

 

예제2: 와일드 카드 * 를 이용해서 여러 인덱스 검색하기

GET logs-2021-*/_search

 

 ❗ _all 

  • 인덱스 이름 대신에 _all 지정자를 사용하여 GET _all/_search 를 실행하면 클러스터에 있는 모든 인덱스를 대상으로 검색할 수 있다.
  • 하지만 _all 은 시스템 사용을 위한 인덱스 같은 데이터도 모두 접근하여 불필요한 작업까지 하므로, 되도록이면 _all은 사용하지 않도록 하자.

 

 

5. 검색과 쿼리: Query DSL

Intro

검색(search)의 정의

데이터 시스템에서의 검색이란 

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

라고 정의할 수 있다.

인터넷 쇼핑몰 검색을 예로 들어보자. 상품이 100만 개가 있을 때 검색창에 "무선 이어폰"이라 입력해서 시스템에 있는 전체 100만개의 상푿믈 중 무선 이어폰과 연관된 상품들만 추려내는 과정이 바로 검색이라할 수 있다. 검색 엔진을 어떻게 설정하느냐에 따라서 상품명이 정확히 "무선 이어폰"인 것만 보여줄지, 혹은 "소니 무선 이어폰"처럼 전체 상품명 중 검색어를 포함하기만 한 상품을 모두 보여줄지, 혹은 가격, 출시일 등 다른 조건들도 영향을 받게할지 등을 결정할 수 있다.

만약 상품명이 정확히 "무선 이어폰"인 것만 검색하도록 조건 설정하면 결과수가 적어져서 찾고자하는 상품이 나오지 않을 수도 있다. 그렇다고 상품 설명에 "무선"과 "이어폰"이 하나라도 포함된 상품을 모두 검색한다면 "무선 리모컨", "이어폰 케이스"와 같이 연관없는 검색들까지 검색이 되면서 결과가 너무 많아져버리는 바람에 찾는 상품이 묻혀질 수도 있다.

 

풀 텍스트 검색 (Full Text Search)

Elasticsearch는 여러가지 검색 조건에 따른 검색 기능을 구현할 수 있도록 다양한 기능들을 제공한다. 그 중에서도 Elasticsearch는 풀 텍스트 검색도 지원한다. 풀 텍스트 검색이란 다음과 같다.

  • 데이터를 검색할 때 실제로 사용되는 검색어인 텀(Term)으로 분석하고 저장한다.
  • 따라서 검색 시 대소문자, 단수나 복수, 원형 여부와 상관없이 검색이 가능하다.

 

Query DSL (Domain Specific Language)

  • 데이터 시스템에서 제공하는 검색하기 위한 쿼리 기능
  • Elasticsearch의 Query DSL은 모두 json 형식으로 입력해야 한다.

 

5-1. 풀 텍스트 쿼리 (Full Text Query)

도큐먼트 입력하기

Elasticsearch 검색에 사용되는 주요 쿼리들을 살펴보자. 예제를 실행하기 위해서 my_index 인덱스에 다음의 5개 도큐먼트를 입력하자.

// my_index 인덱스에 벌크 API로 5개 도큐먼트(데이터) 입력하기
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"}

 

1) match_all

  • 아무 조건 없이 해당 인덱스 내의 모든 도큐먼트를 검색
  • 쿼리를 넣지 않았을 떄 자동으로 match_all을 적용하여 해당 인덱스의 모든 도큐먼트를 검색한다.

 

예제

아래 두 예제는 결과가 동일하다.

// 쿼리 없이 실행
GET my_index/_search
// match_all 쿼리로 실행
GET my_index/_search
{
   "query": {
      "match_all": { }
   }
}

 

2) match

  • 풀 텍스트 검색에 사용되는 가장 일반적인 쿼리
  • field: value 값을 검색한다.

 

예제1:

아래 예제는 match 쿼리를 이용해서 my_index 인덱스의 message 필드에 dog가 포함되어 있는 모든 문서를 검색한다.

request

// match 쿼리로 message 필드에 있는 dog 검색하기
GET my_index/_search
{
   "query": {
      "match": {
         "message': "dog"
      }
   }
}

response

// match 쿼리로 message 필드에서 dog 검색 결과
{
  "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"
        }
      }
    ]
  }
}

dog가 포함된 문서는 총 4개이다.

 

예제2: 여러 검색어 넣기

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

request

// match 쿼리로 message 필드에서 quick dog 검색하기
GET my_index/_search
{
  "query": {
    "match": {
      "message": "quick dog"
    }
  }
}

response

// match 쿼리로 message 필드에서 quick dog 검색한 결과
{
  "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"
        }
      }
    ]
  }
}

quick와 dog 둘 중 하나라도 포함된 도큐먼트들이 모두 검색된다. 그 결과 총 5개 도큐먼트가 검색되었다.

 

예제3: operator 옵션

  • 만약 검색어가 여럿일 떄 검색 조건을 OR 이 아니라 AND로 검색하고 싶다면 operator 옵션을 사용하면 된다.
  • 문법
    • before: <필드명>:<검색어>
    • after: <필드명>: {"query": <검색어>, "operator": <operator> }

quick dog를 AND 조건으로 검색하려면 다음과 같다.

request

// match 쿼리 AND 조건으로 quick dog 검색하기
GET my_index/_search
{
  "query": {
    "match": {
      "message": {
        "query": "quick dog",
        "operator": "and"
      }
    }
  }
}

response

// match 쿼리 AND 조건으로 quick dog 검색 결과
{
  "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"
        }
      }
    ]
  }
}

 

3) match_pharse

  • 입력된 검색어 구문 전체 그대로를 검색한다.
  • "quick dog"라는 구문을 공백을 포함해서 정확히 일치하는 내용을 검색하고 싶을 때 사용한다.

 

예제1

다음 match_pharse 쿼리는 lazy dog라는 구문을 검색한다.

request

// match_pharse 쿼리로 "lazy dog" 구문 검색
GET my_index/_search
{
  "query":{
    "match_phrase": {
      "message": "lazy dog"
    }
  }
}

response

// match_pharse 쿼리로 "lazy dog" 구문 검색 결과
{
  "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"
        }
      }
    ]
  }
}

 

예제2: slop 옵션 사용하여 검색하기

match_pharse 쿼리는 slop 옵션을 사용하여 slop에 지정된 값만큼 단어 사이에 다른 검색어가 끼어드는 것을 허용할 수 있다. slop을 1로 두고 검색하면 다음과 같은 결과가 나온다.

request

// match_pharse 쿼리에 slop:1로 "lazy dog" 구문 검색하기
GET my_index/_search
{
  "query":{
    "match_phrase": {
      "message":{
        "query": "lazy dog",
        "slop": 1
      }
    }
  }
}

response

// match_pharse 쿼리에 slop:1로 "lazy dog" 구문 검색 결과
{
  "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"
        }
      }
    ]
  }
}

slop을 1로 두었기 때문에 lazy와 dog 사이에 jumping이 있는 "Lazy jumping dog"값도 검색된다. slop을 2로 둔다면 lazy jumping brow dog와 같은 문장도 검색될 것이다.

 

이처럼 match_pharse 쿼리는 slop를 이용하면 정확도를 조절해가면서 원하는 검색 결과의 범위를 넓힐 수 있다. slop을 너무 크게하면 검색 범위가 넓어져서 관련 없는 결과가 나타날 확률도 높아지므로 1을 사용하는 것을 권장한다.

 

4) query_string

4-4. 검색 API 에서 URL에 q 파라미터를 사용해서 검색해보았다. URL 검색에 사용하는 루씬의 검색 문법으로 본문 검색하고 싶을 때 query_string을 사용한다.

 

예제

다음은 message 필드에서 lazy와 jumping을 모두 포함하거나, 또는 'quick dog" 구문을 포함하는 도큐먼트를 검색하는 쿼리이다. match_pharse 처럼 구문을 검색할 때는 쌍따옴표 \" 안에 넣는다.

request

// query_string 쿼리 검색
GET my_index/_search
{
  "query": {
    "query_string": {
      "default_field": "message",
      "query": "(jumping AND lazy) OR \"quick dog\""
    }
  }
}

response

// query_string 쿼리 검색 결과
{
  "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"
        }
      }
    ]
  }
}

결과로 "Lazy jumping dog" 도큐먼트와 "quick dog" 값을 포함하는 두 개의 도큐먼트가 검색된다.

 

5-2. Bool 복합 쿼리 (Bool Query)

Bool Query란?

query_string 쿼리는 여러 조건을 조합하기엔 쉽지만 옵션이 한정된다.

본문 검색(body search)에서 여러 쿼리를 조합하려면 위에서 bool 쿼리를 사용하고 그 안에 다른 쿼리들을 넣는 방법을 주로 사용한다. bool 쿼리는 4개의 인자를 가지고 이씨고, 그 인자 안에 다른 쿼리들을 배열로 넣어 동작한다.

  1. must : 쿼리가 참인 도큐먼트들을 검색한다.
  2. must_not : 쿼리가 거짓인 도큐먼트들을 검색한다.
  3. should : 검색 결과 중에서 이 쿼리에 해당하는 도큐먼트의 점수를 높인다.
  4. filter : 쿼리가 참인 도큐먼트를 검색하나 스코어를 계산하지 않는다. must보다 검색속도가 빠르고 캐싱이 가능하다.

 

bool 쿼리 사용 문법

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

 

예제1: "quick"과 구문 "lazy dog"가 포함된 모든 문서 검색하기

request

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

response

{
  "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"
        }
      }
    ]
  }
}

 

예제2: 단어 "quick"와 구문 "lazy dog"가 하나도 포함되지 않는 문서 검색하기

request

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

response

{
  "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"
        }
      }
    ]
  }
}

 

bool 쿼리 (must, must_not, should, filter) vs SQL (AND, OR, NOT, ...)

이처럼 bool 쿼리를 사용하면 query_string처럼 복합적인 검색 기능을 구현할 수 있다. 특히 bool 쿼리는 정확도(Relevancy) 예제에서 필요하다. (후에 5.3에서 살펴볼 것임!)

bool 쿼리의 must, must_not, should, filter는 SQL의 AND, OR, NOT, 등과 비슷해보이지만 정확하게 같진 않다. must는 SQL의 AND 연산자와 유사하게 동작한다. 하지만 SQL의 OR 연산자는 bool 쿼리와 일치하게 동작하는 연산자가 없어서 헷갈릴 수 있다...

SQL의 AND, OR 조건들은 2개의 조건값에 대한 이항 연산자이다. 하지만 Elasticsearch의 must, must_not, should, filter는 내부에 있는 각 쿼리들에 대해 해당 쿼리의 참 거짓임을 적용하는 단항 연산자이다. 

표준 SQL과 Elasticsearch Bool 쿼리 비교

 

여기서 (A or B) and (not C) 를 쿼리로 하려면 elasticsearch의 경우에는 다음처럼 A와 B의 OR 조건의 match 쿼리를 하여 must 안에 넣고, C를 must_not에 넣으면 된다.

표준 SQL과 Elasticsearch Bool 쿼리 비교

 

 

5-3. 정확도 (Relevancy)

RDBMS 같은 시스템은 쿼리 조건에 맞는 결과만 가져올 뿐, 각 결과들이 얼마나 정확한지는 모른다. Elasticsearch와 같은 풀 텍스트 검색엔진은 검색 결과가 입력된 검색 조건과 얼마나 정확히 일치하는지 계산하는 알고리즘을 갖고 있기 때문에 이 정확도를 기반으로 사용자가 가장 원하는 결과를 먼저 보여줄 수 있다. 이 정확도를 relevancy라 한다.

검색할 때 사용자는 정확한 결과만 보고 싶어 한다. 검색 조건에 포함되더라도 사용자가 찾으려는 결과와 상관 없는 결과는 보여주지 않는 것이 좋다. 따라서 이 정확도가 필요한 것이다. 구글이나 네이버 같은 웹 검색엔진도 검색 후 찾은 결과들 중에서 어떤 결과가 사용자가 입력한 검색어와 가장 연관성 있는지 계산하고, 정확도가 가장 높은 결과부터 순서대로 보여준다.

 

스코어(score) 점수

Elasticsearch의 검색 결과에는 스코어 점수가 표시된다. 이는 검색된 결과가 얼마나 검색 조건과 일치하는지를 나타내고, 점수가 높은 순으로 결과를 보여준다.

 

예제1: match 쿼리 결과

request

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

response

{
  "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"
        }
      }
    ]
  }
}

각 검색 결과에는 _score 항목에 스코어 점수가 표시되고, 이 점수가 높은 결과 순서대로 나타난다. 또한 상단의 max_score 에는 전체 결과 중 가장 높은 점수가 표시된다. Elasticsearch 는 BM25 (Best Matching) 알고리즘으로 이 점수를 계산한다.

 

BM25 계산식

복잡해 보이지만, 크게 3가지 요소가 사용된다.

  1. TF (Term Frequency) : 검색된 텀이 많을 수록 점수가 높아짐
  2. IDF (Inverse Document Frequency)
  3. Field Length :

 

1) TF (Term Frequency)

구글에서 '쥬라기 공원'를 검색한다고 하자. 그러면 "쥬라기 공원" 단어가 5번 들어가 있는 웹 페이지보다는 10번 들어간 웹 페이지에 내가 보고 싶은 정보가 있을 확률이 높을 것이다. 이처럼 도큐먼트 내에 검색된 텀(Term)이 더 많을 수록 점수가 높아지는 것을 Term Frequency라 한다.

앞의 검색에서는 'The quick brown fox jumps over the quick dog"인 도큐먼트가 텀 quickdog 총 세 개를 포함하고 있어 가장 점수가 높다. 텀을 많이 포함할 수록 위의 그래프처럼 TF 값도 증가한다. BM25에서는 최대 25까지 증가하고, 25이상부터는 TF 점수에 변화가 없다.

 

2) IDF (Inverse Document Frequency)

"쥬라기 공원" 검색어에서 "쥬라기" 또는 "공원" 둘 중 어떤 단어를 포함하는 페이지는 검색 결과에 나타날 수 있다. 이 때 전체 검색결과 중에서 "쥬라기"가 포함된 결과는 10개, "공원"이 포함된 결과는 100개라 한다면 "공원"보다는 더 적게 발견된(희소한) "쥬라기" 검색이 더 중요한 텀일 가능성이 높다. 따라서 검색한 텀을 포함하는 도큐먼트 개수가 많을 수록 그 텀 자체의 점수가 감소하는데, 이를 Inverse Document Frequency라 한다.

 

앞의 검색에서는 "The quick brown fox" 도큐먼트와 "Lazy jumping dog" 도큐먼트는 각각 quick와 dog가 한 번씩 들어간다. 하지만 전체 인덱스에서 quick이 들어간 문서는 3개, dog이 들어간 문서는 4개이므로 quick이 들어간 결과의 점수가 더 높다. 따라서 전체 인덱스에 포함된 텀이 증가할수록 위 그래프처럼 IDF는 감소한다. (그래서 Inverse다)

 

3) Field Length

도큐먼트에서 필드 길이가 긴 필드보다는 짧은 필드가 텀의 비중이 클 것이다. 예로 들어서 블로그 포스트를 검색하는 경우, 검색하는 단어가 제목과 내용 필드에 모두 있는 경우에 텍스트 길이가 긴 내용 필드보다는 텍스트 길이가 짧은 제목 필드에 검색어를 포함하고 있는 블로그 포스트가 점수가 더 높다. 

lazy를 검색한 쿼리 결과를 살펴보자.

request

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

response

{
  "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"
        }
      }
    ]
  }
}

두 도큐먼트 모두 lazy를 포함하고 있지만, 길이가 더 짧은 도큐먼트가 점수를 더 높게 받았다.

 

5-4. Bool: Should

5.2에서 bool 쿼리에 대해서 살펴봤었다. bool 쿼리의 4개의 인자를 다시 살펴보자.

  1. must : 쿼리가 참인 도큐먼트들을 검색한다.
  2. must_not : 쿼리가 거짓인 도큐먼트들을 검색한다.
  3. should : 검색 결과 중에서 이 쿼리에 해당하는 도큐먼트의 점수를 높인다.
  4. filter : 쿼리가 참인 도큐먼트를 검색하나 스코어를 계산하지 않는다. must보다 검색속도가 빠르고 캐싱이 가능하다.

이 중에서 shnould는 검색 점수를 조정하기 위해 사용된다.

 

예제: match 쿼리로 fox를 포함한 도큐먼트 검색하기

request

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

response

{
  "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"
        }
      }
    ]
  }
}

여기서 lazy가 포함된 결과에 더 가중치를 둬서 상위로 올려보자. 그러면 bool의 should 안에 lazy를 찾는 검색을 추가한다.

request: fox 검색 결과 중 lazy를 포함한 결과에 가중치 부여하기

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

response

{
  "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"
        }
      }
    ]
  }
}

검색 결과, fox만 포함하는 도큐먼트들은 점수가 이전과 동일하지만, lazy를 포함하는 도큐먼트는 점수가 이전보다 높아져서 가장 상위에 나타난다.

 

should + match_pharse

should는 match_pharse와 함께 유용하게 사용할 수 있다. 예로 들어서 쇼핑몰 상품 검색의 경우에는 보통 검색어로 입력된 단어가 하나라도 포함된 결과들은 모두 가져오도록 되어 있다. 이때 검색 결과 중에서 입력한 검색어 전체 문장이 정확히 일치하는 결과를 맨 상위에 위치시키고 다른 결과들을 누락시키지 않으면서 사용자가 정확하게 원하는 결과를 제공할 수 있다.

예제: lazy 또는 dog 둘 중 하나라도 포함된 도큐먼트를 모두 검색하면서, 그 중에서 'lazy dog' 구문을 정확히 포함하는 결과들을 가장 상위로 가져오기

must 안에 match 쿼리로 lazy 혹은 dog가 포함된 모든 도큐먼트를 검색하고, should 안에는 match_pharse를 써서 스코어 점수를 높인다.

request

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

response

{
  "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"
        }
      }
    ]
  }
}

  • 이렇게 should와 match_pharse를 사용하면 쇼핑몰에서 "스키 장갑" 같은 단어로 검색했을 때 스키 용품들과 각종 장갑들을 모두 가져오면서 그 중에서 스키 장갑을 가장 상단에 표시할 수 있다.
  • 또한 slop: 1를 이용하면 '스키 보드 장갑', '스키 벙어리 장갑'과 같이 스키와 장갑 사이 다른 값이 들어간 결과에도 가중치를 부여할 수 있다.

 

5-5. 정확값 쿼리 (Exact Value Query)

정확값(Exact Value)란?

지금까지는 스코우 점수, 즉 정확도(relevancy)가 높은 결과 순서대로 가져오는 풀 텍스트 검색을 살펴보았다. 이 외에도 Elasticsearch는 검색 조건의 참/거짓 여부만 판별하여 결과를 가져올 수 있는데, 이를 정확값(Exact Value)라 한다.

  • 말 그대로 값이 정확히 일치하는지 여부만을 따지는 검색이다.
  • term, range와 같은 쿼리들이 있다.
  • 스코어를 계산하지 않으므로 보통 bool 쿼리의 filter 내부에서 사용한다.

 

1) bool: filter

  • bool 쿼리의 filter 안에 하위 쿼리를 사용하면 스코어에 영향주지 않는다.

 

예제: bool filter 사용법

다음 3개의 검색 결과를 비교해보자.

  1. match 쿼리로 fox 검색
  2. match 쿼리로 fox와 quick 검색
  3. must로 fox, filter로 quick 검색

1) match 쿼리로 fox 검색

request

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

response

{
  "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"
        }
      }
    ]
  }
}

 

2) match 쿼리로 fox와 quick 검색

request

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

response

{
  "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"
        }
      }
    ]
  }
}

 

3) must로 fox, filter로 quick 검색

request

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

response

{
  "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"
        }
      }
    ]
  }
}

 

첫 번째: match 쿼리로 fox를 검색했을 때

  • 4개의 도큐먼트 검색됨
  • 가장 높은 스코어는 "_score": 0.32951736

두 번째: match 쿼리로 quick를 추가했을 때

  • 3개의 도큐먼트 검색됨
  • 가장 높은 스코어는 "_score": 0.9468958

세 번쨰: 첫 번째 검색에서 filter 구문 내에 quick 추가했을 때

  • 3개의 도큐먼트 검색됨
  • 가장 높은 스코어는 "_score": 0.32951736 (첫 번째와 동일)

이처럼 filter는 검색에 조건을 추가하되 스코어에는 영향을 주지 않는다. 쇼핑몰에서 검색어로 정확도가 높은 상품명을 검색하면서 생산 업체를 다시 필터링하는 등의 용도로 사용할 수 있다.

 

예제2: bool filter 내부에 다른 bool 쿼리 추가하기

filter 내부에서 must_not과 같이 다른 bool 쿼리를 포함하려면, filter 내부에 bool 쿼리를 먼저 넣고 그 안에 must_not을 넣으면 된다. 

다음 예제는 fox를 포함하면서 dog는 포함하지 않는 도큐먼트를 검색하는 쿼리다. filter 안에 dog를 제외하는 must_not 쿼리가 있으므로 fox만 스코어에 영향을 받는다.

(➕)

  • 쿼리문을 작성할 때 항상 처음에 "query'를 작성해야 함
  • match, match_all, match_pharse처럼 기본 쿼리 조건만 사용하는 게 아니라, 다양한 쿼리문을 복합적으로 사용하려면 "bool" 쿼리를 작성함
  • bool 쿼리에는 "must", "must_not", "filter", "should"가 있음
  • 이 중에서 "filter"는 스코어에 영향을 주지 않고 검색하는 방식인데, 스코어에 영향을 주지 않으면서 다양한 검색을 하려면 filter 내에서 또 "bool" 쿼리를 작성해 주어야 함
  • 그래서 아래와 같은 형식이 된다.

request

// must로 fox 검색 후 must_not으로 dog 제거하기
GET my_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "message": "fox"
          }
        }
      ],
      "filter": [
        {
          "bool": {
            "must_not": [
              {
                "match": {
                  "message": "dog"
                }
              }
            ]
          }
        }
      ]
    }
  }
}

response

{
  "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"
        }
      }
    ]
  }
}

 

2) keyword

  • 문자열 데이터를 keyword 형식으로 저장하면 정확값 검색을 할 수 있다.
  • keyword 형식으로 저장된 필드는 스코어를 계산하지 않고 정확값의 일치 여부만 따진다.
  • 스코어를 계산하지 않으므로 keyword 값을 검색할 때는 filter 구문 안에 넣도록 한다.

예제: message 필드값이 "Brown fox brown dog" 문자열과 공백, 대소문자까지 정확히 일치하는 데이터만 가져오기

request

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

response

{
  "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"
        }
      }
    ]
  }
}

스코어에 영향을 미치지 않으므로 "_score": 0.0이 나온다.

 

 

5-6. 범위 쿼리 (Range Query)

ㅇㅇ

 

 

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

6-1. 역 인덱스 (Inverted Index)

ㅇㅇ

 

6-2. 텍스트 분석 (Text Analysis)

ㅇㅇ

 

6-3. 애널라이저 (Analyzer)

ㅇㅇ

 

6-4. 캐릭터 필터 (Character Filter)

ㅇㅇ

 

6-5. 토크나이저 (Tokenizer)

ㅇㅇ

 

6-6. 토큰 필터 (Token Filter)

ㅇㅇ

 

6-7. 형태소 분석 (Stemming)

ㅇㅇ

 

 

 

7. 인덱스 설정과 매핑 (Settings & Mappings)

7-1. 설정 - Settings

ㅇㅇ

 

7-2. 매핑 - Mappings

ㅇㅇ

 

7-3. 멀티(다중) 필드 - Multi Field

ㅇㅇ

 

 

 

8. 집계 (Aggregations)

8-1. 메트릭 (Metrics Aggregations)

ㅇㅇ

 

8-2. 버킷 (Bucket Aggregations)

ㅇㅇ

 

8-3. 하위 (sub-aggregations)

ㅇㅇ

 

8-4. 파이프라인 (Pipeline Aggregations)

ㅇㅇ

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

elasticsearch.bat 실행 후 localhost:9200 접속

 

인덱스 생성

cmd에서 curl -XPUT localhost:9200/meta1

인덱스 조회

(1) curl로 조회하기

cmd에서 curl -XGET localhost:9200/meta1?pretty (pretty로 더 예쁘게 출력)

(2) 브라우저로 조회하기

localhost:9200/meta1 접속 시

데이터 입력 (curl 명령어로)

(1) 데이터 입력

➡ log-2019-07-15 의 1에 데이터 입력

curl -XPUT localhost:9200/meta1/log-2019-07-15/1?pretty  -H "Content-Type: application/json" -d "{\"projectName\":\"ControlAuto\",\"logType\":\"ASN.1\",\"logSource\":\"namenode\",\"logTime\":\"2019-07-15T16:23:12\",\"host\":\"host1\"}"

(2) 데이터 조회

curl -XGET localhost:9200/meta1/log-2019-07-15/1?pretty

 

인덱스 삭제

curl -XDELETE localhost:9200/meta1?pretty

그리고 다시 데이터 조회

 

 


참고 자료

- windows에서 elasticsearch 설치하기 : https://man-tae.tistory.com/6

- elasticsearch 시작하기 : https://12bme.tistory.com/171

- elastic 가이드북 : https://esbook.kimjmin.net/02-install/2.2/2.2.1-download-install

- 자바 환경변수 설정 : https://hyoje420.tistory.com/7

  • 근데 이렇게 자바 환경변수를 JAVA_HOME으로 설정하고 elasticsearch.bat를 실행시키면, JAVA_HOME is depcrated어쩌고 뜨면서 ES_JAVA_HOME으로 설정하라고 뜬다. 그래서 ES_JAVA_HOME 이름을 환경변수로 설정해야 한다.

- Elasticsearch 기본 용어들 정리 (클러스터, 노드, 인덱스, 타입, 도큐먼트, 샤드, 리플리카) : https://hsunnystory.tistory.com/175  / https://www.elastic.co/guide/kr/elasticsearch/reference/current/gs-basic-concepts.html

 

반응형

'Back-End' 카테고리의 다른 글

[작성중]elasticsearch putty  (0) 2021.10.29
Elasticsearch 설명서와 함께하는 Elasticsearch 기본 개념 살펴보기  (0) 2021.10.20
JavaScript TDD  (0) 2021.09.29
SQA, CI/CD, TDD  (0) 2021.09.27
[mongoDB] $inc  (0) 2021.09.24