다나와 서비스의 상품갯수는 급격하게 늘어나고 있습니다. 몇달전 4~5억건이었던 상품이 7억건에 육박하고 있는데요. 현재 사용하고 있는 패스트캣 검색엔진에서는 대량의 문서들을 여러개의 인덱스로 나누어 색인하고 있습니다.
검색할때는 나눈 인덱스들을 하나로 합쳐서 검색하고 있는데요. 엘라스틱서치에도 여러 인덱스를 하나로 합쳐서 한번에 검색하는 기능이 있어서 지금처럼 인덱스를 쪼개서 사용하는 것도 가능한 시나리오입니다. 참고로 얘기하면, 이렇게 데이터를 범위나 카테고리로 나누어 관리하는 기법을 파티셔닝이라고 합니다.
먼저 인덱스를 하나로 가져가든 여러개로 나누든, 샤드 하나의 크기는 비슷하게 설정해야 할텐데요, 적절한 샤드의 크기를 먼저 정해야 할 것 같습니다. 몇차례 구글링을 한결과 엘라스틱서치의 공식블로그에서 도움이 될만한 글를 찾을수 있었습니다.
TIP: 작은 샤드는 작은 세그먼트를 만들며 부하를 증가시킵니다. 평균 샤드 크기를 최소한 수 GB와 수십 GB 사이를 유지하세요. 시간 기반 데이터를 사용한 과거 사례를 보면, 20GB ~ 40GB 정도의 사이즈가 적당합니다.
샤드크기는 수GB에서 수십GB 사이가 적당하며, 경험상 시계열 데이터의 경우 20GB~40GB가 적당하다고 합니다. 그리고 아래에서 또다른 팁을 발견할수 있었습니다.
TIP: 하나의 노드에 저장할 수 있는 샤드의 개수는 가용한 힙의 크기와 비례하지만, Elasticsearch에서 그 크기를 제한하고 있지는 않습니다. 경험상 하나의 노드에 설정한 힙 1GB 당 20개 정도가 적당합니다. 따라서 30GB 힙을 가진 노드는 최대 600개 정도의 샤드를 가지는 것이 가능하지만, 이 보다는 적게 유지하는 것이 더 좋습니다.
힙메모리 30GB로 엘라스틱서치를 구동시 샤드는 최대 600개 정도라고 합니다. 이미 32GB 를 힙에 할당할 생각을 하고 있으므로, 활성화된 인덱스가 50개라고 한다면, 인덱스당 600샤드 / 50 인덱스 = 12 로 인덱스당 최대 12개의 샤드를 설정할수 있을 겁니다. 일단 샤드갯수의 최대치는 넉넉한것 같으므로, 많아서 문제가 될것같지는 않습니다.
하나의 샤드를 20GB~40GB로 설정한다면, 7억건의 상품은 몇개의 샤드로 나눠지게 될까요? 다나와 상품기준으로는 15개의 샤드로 나눠지게 됩니다. 물론 엘라스틱 서치 블로그에서는 시계열 데이터에 대한 팁이라서 상품과는 문서의 특성이 다릅니다. 대부분은 웹서버나 DB 로그데이터 일텐데요, 로그스태시의 파싱모듈을 확인해보면 한 로그내의 필드수가 10개 내외로 매우 적습니다. 반면에 상품의 필드는 수십개는 기본입니다.
이러한 문서특성의 차이로 검색과 색인시 한개 문서에 들어가는 리소스와 시간이 더 투여됩니다. 그러므로, 상품문서는 하나의 샤드를 20GB 보다는 조금 더 작게 나누면 여러 서버에 더 잘게 분산이 되어 전체적인 응답시간은 더 빨라질겁니다. 우리는 샤드당 약 5GB로 설정하고 테스트 했는데, 빠른 응답속도를 얻을 수 있었습니다.
그럼, 인덱스를 나누는것과 샤드를 나누는것 어떤것이 적합할까요? 어치피 샤드는 여러 서버에 분산되어 병렬 및 병행 (참고: https://soy.me/2015/01/03/concurrent/)으로 검색이 되므로, 인덱스가 같던 다르던 상관은 없습니다. 검색의 기본 단위는 샤드이기 때문이죠. 따라서 인덱스를 나누는 것은 운영의 편의성을 고려할때 선택하는 방법입니다. 장애없는 운영의 측면에서 생각해보면, 큰 덩어리 하나를 다루는 것은 부담스러워도, 작은 덩어리 여러개를 다루는 것이 번거롭긴해도 그만큼 장애 가능성을 낮추는 방법이 됩니다.
전체색인을 빠르게 하려면
전체색인을 할 경우 인덱스 하나가 7억건이라면 색인이 모두 끝날때까지 약 3-4시간이 소요될것이고, 검색에 노출되기 까지 이시간을 고스란히 기다려야 합니다. 더 문제가 되는것은 전체색인 도중에도 상품의 가격은 계속해서 변하게 되는데 이 변경분을 전체색인후에 일괄적용을 해야하며, 대기시간이 길수록 일괄적용시간도 늘어나게 됩니다. 그러므로, 최대한 전체색인을 빠르게 해야하는데, 이를 위해서는 문서를 입력하는 REST 클라이언트를 멀티스레드로 여러개 생성하여 동시입력량을 늘려야 합니다. 이때 Bulk Insert 를 쓰는것은 기본이구요.
결국은 더 빠른 색인을 위해서는 하나의 인덱스를 여러개로 나눠야 합니다. 통으로 4시간이 걸리는 문서를 10개의 인덱스로 나누면 색인시간이 서버를 공유하므로 정확히 1/10이 될수는 없지만, 그대로 각각 40분정도로 병행완료가 됩니다.
상품DB의 특성상 우리는 카테고리군별로 인덱스를 나누고 있습니다. 카테고리별로 인덱스를 나눌때의 장점은 특정카테고리만 검색할때 해당 인덱스만 검색하면 되므로, 검색부하가 현저히 감소하게 됩니다. 우리는 인덱스도 나누고 샤드도 2-3개로 나누어 전체적인 검색응답시간을 최대한 단축하는 방향으로 설계했습니다.
레플리카 갯수는 몇개로?
고가용성을 추구할때 빼놓을 수 없는 것이 레플리카인데요, 레플리카는 레플리카 샤드를 줄여서 얘기하는 것으로, 프라이머리 샤드에 종속됩니다. 우리말로는 복제본 이라고도 하죠. 복제본은 분산 데이터 시스템에서는 동일한 역할을 담당하는데요, 하드웨어 장애를 극복하고 검색과 같은 읽기처리량을 향상시키는 것입니다.
그런데 읽기성능이 좋아진다는 것은 쓰기성능이 낮아진다는 얘기도 됩니다. 왜냐하면, 복제본을 여러개 만들기 위해서는 그만큼 문서색인시에 쓰기작업도 복제본 갯수만큼 발생하기 때문이죠. 하나의 서버만 본다면 1이지만, 전체클러스터 입장에서는 복제본이 열개면 10의 쓰기가 일어나는 것입니다. 문서가 색인될때 구지 모든 서버의 IO발생하게 만들 필요는 없을겁니다.
또한 복제본의 갯수는 동시에 장애가 발생할 노드를 몇개 까지 허용하는지에 달려있습니다. 복제본이 1개라면 하나의 노드가 죽어도 검색서비스는 유지됩니다. 하지만, 이제 복제본은 0개가 되므로, SA가 빨리 노드를 복구하지 않으면, 다른 또하나의 노드가 죽을때 검색서비스에는 장애가 생깁니다.
만약 회사내에 장애 복구시스템이 잘 갖춰져 있다고 하면, 복제본은 1개로도 가능합니다. 복제본이 2라면 서버가 연속 2개 죽어도 상관이 없으므로, 마음을 느긋하게 가질수 있습니다. 하지만 전체 클러스터에서 서버가 2개 빠지고도 부하를 충분히 견딜수 있게 서버를 배치해 놓아야 합니다. 우리는 카테고리별 인덱스의 읽기성능을 고려해서 부하가 높은 인덱스는 복제본 2를 나머지는 1을 설정할 계획입니다.
결론
nginx로그와 같은 로그성 문서는 색인을 하고 나면 수정이 필요없는 정적인 컨텐츠인 반면에, 상품문서는 색인이 끝나도 계속 갱신이 되어야 하는 살아있는 컨텐츠입니다. 그렇기 때문에, 동적색인이 원활하고 장애도 대비할수 있으며, 검색성능도 높은 설계가 필요합니다. 그리고 엘라스틱서치를 사용하여 검색시스템을 설계할때 운영자가 할수 있는 최선의 방법은 문서와 샤드를 잘 관리하는 것입니다. 따라서 견고한 검색시스템을 위해 인덱스와 샤드를 어떻게 설정해야 하는지에 대해 살펴봤습니다.
Elasticsearch, Logstash, Kibana 오픈 소스 프로젝트 3개를 합쳐서 ELK 라고 한다. Elasticsearch 는 검색 및 분석 엔진으로 무료버전으로도 기본적인 기능은 충분히 사용가능하다. Logstash는 여러 소스에서 동시에 데이터를 수집하여 변환한 후 Elasticsearch 와 같은 곳에 ‘stash’로 전송하는 서버 사이드 데이터 처리 파이프라인을 말한다. Kibana 는 사용자가 Elasticsearch 를 시각화하여 사용할수 있는 툴이다.
여기서는 이미 사용하고 있는 docker 기반 mySql 을 Elasticsearch 에 logstash 를 이용해 동기화 하고, 검색하는 부분만 포스팅할 예정이다.
2. Nori ?
한글 검색을 올바르게 수행하기 위해서는 형태소 분석기 plug-in 을 Elasticsearch 에 따로 설치해주어야 하는데, 다른 다양한 형태소 분석기들도 많지만 필자는 Elasticsearch 공식문서를 보면서 작업하기 수월하기 위해 Nori 형태소 분석기를 사용하기로 하였다. nori는 elasticsearch 가 실행된 상태에서 해당 bash 에 접속해 설치하면 된다.
$ docker exec -it elasticsearch /bin/bash
$ bin/elasticsearch-plugin install analysis-nori #elasticsearch container 에서 명령
nori를 이용한 검색을 해야하는 데이터 컬럼에 대해서는 반드시 mapping analyzer 셋팅을 해주어야 한다. 예를들어 ‘name’이라는 column 에 형태소 분석을 이용한 한글검색이 가능하게 하고자 한다면 mapping 은 다음과 같은 형태일 수 있다.
version:'3.2'services:elasticsearch:image:docker.elastic.co/elasticsearch/elasticsearch:7.6.1#기본 이미지 사용container_name:elasticsearchenvironment:ES_JAVA_OPTS:"-Xmx256m -Xms256m"discovery.type:single-node#간단한 local 테스트를 위해 single-node로 구성volumes:-/usr/share/elasticsearch/dataports:-9200:9200networks:-elklogstash:container_name:logstashimage:docker.elastic.co/logstash/logstash:7.6.1environment:LS_JAVA_OPTS:"-Xmx256m -Xms256m"volumes:-type:bindsource:./config/logstash/config/logstash.ymltarget:/usr/share/logstash/config/logstash.ymlread_only:true-type:bindsource:./config/logstash/pipelinetarget:/usr/share/logstash/pipelineread_only:true-./jar/mysql-connector-java-8.0.18.jar:/usr/share/logstash/logstash-core/lib/jars/mysql-connector-java-8.0.18.jar#mysql 사용을 위한 jdbc-connector 다운networks:-elkdepends_on:-elasticsearch#elasticsearch 가 실행된 이후 logstash 실행되도록 depends_on 설정kibana:container_name:kibanaimage:docker.elastic.co/kibana/kibana:7.6.1ports:-5601:5601networks:-elkdepends_on:-elasticsearch#elasticsearch 가 실행된 이후 kibana 실행되도록 depends_on 설정networks:elk:external:name:mysql-network#이미 사용하고 있는 db와 연결을 위해 mysql-docker network 와 연결이 되도록 external 설정을 따로 해주었다. volumes:elasticsearch:#volume container 사용
이미 떠있는 mysql container 와의 연결을 위해 external network 설정을 따로 해주었으나, 만약 elk 구축과 함께 database 를 실행한다면 external 연결은 해줄필요가 없다. 기본적인 ELK 설정을 거이 따르고 있으며 여기서는 mysql 연결을 위한 부분과 single-node 사용하는 부분만 수정하였다.
4. mysql 과 동기화 작업 설정
동기화 하고 싶은 데이터가 있다면 logstash.conf 파일에서 설정해주면 된다. 여기서는 crontab 과 수정일을 이용하여 마지막 데이터를 동기화해 가지고 오도록 작업하였다.
[대괄호]안에 있는 부분들은 해당 상황에 맞게 입력하면 되는 값이며, 여기서는 위에서 언급한것처럼 modification_time 이라는 수정일 컬럼을 이용하여 동기화하였다. 새로 추가되거나, 수정/삭제 된 경우에도 데이터가 자동으로 동기화 되기 위해서 다음과 같이 작업을 진행하였다.
각 환경에 맞게 수정한 docker-compose.yml 파일과 logstash.conf 등 설정파일들 준비가 완료되었다면 docker-compose.yml 파일이 있는 디렉토리에서 docker-compose up 을 통해 해당 파일이 실행되는것을 확인할 수 있으며, localhost:5601 에 접속하여 kibana 에서 편하게 실제 index가 들어간 모습과, mapping 등을 확인할 수 있다.
현재 Microsofts에서 제공하는 도구 모음인 Sysinternalsuite에는 Sysmon이라는 도구가 있다. Sysmon은 윈도우 운영체제에서 높은 수준의 모니터링을 제공한다. 백그라운드에서 실행되며 모니터링 결과를 이벤트 로그에 기록한다. 이러한 유용한 기능을 제공하는 Sysmon은 많이 알려져 있으나 그 결과가 이벤트 로그에 저장되기 때문에 실시간으로 결과값을 보는 것이 기존 SIEM이나 다른 장비에 연동하기 어렵다면 모니터링이 불편하였다. 하지만, 오픈소스인 ELK Stack의 다양한 기능을 이용한다면 이점을 커버할 수 있다.
이번 포스팅에서는 Sysmon에 대하여 알아보고 생성블로그관리된 로그를 ELK Stack을 통해 모니터링 하는 방법에 대하여 알아보고자 한다.
최초 설치에는 “-I” 옵션을 사용했으나 이후 설정파일에 변경이 있다면 “-c” 옵션으로 변경된 옵션을 적용할 수 있다.
설치가 끝나면 자동으로 Sysmon이 작동이 되는데 비스타 이후 버전은 Applications and Services Logs/Microsoft/Windows/Sysmon/Operational 경로에 Sysmon로그가 쌓이며 이전 버전은 System event 로그에 쌓이게 된다. 윈도우에서 기본으로 제공하는 이벤트 뷰어로 확인이 가능하다.
[그림 3] EventLog
Sysmon의 이벤트 로그는 다음과 같이 저장되며 의미를 가진다.
Event ID
설명
1
프로세스 생성
2
프로세스가 파일 생성시간을 변경
3
네트워크 연결
4
Sysmon 서비스 상태 변경
5
프로세스 종료
6
드라이버 로드
7
이미지 로드 (“-l” 옵션으로 활성화 필요)
8
다른 프로세스에서 스레드 생성
9
RawAccessRead 탐지 (프로세스가 “\\.\” 표기법 이용하여 드라이브 접근시)
10
특정 프로세스가 다른 프로세스 Access (ex. Mimikatz > lsass.exe)
11
파일 생성 (생성 및 덮어쓰기)
12
레지스트리 이벤트 (키 값 생성 및 삭제)
13
레지스트리 이벤트 (DWORD 및 QWORD 유형의 레지스트리 값 수정)
14
레지스트리 이벤트 (키 및 값 이름 변경)
15
FileCreateStreamHash (파일 다운로드와 관련된 Zone.identifier 확인)
17
PipeEvent (파이프 생성)
18
PipeEvent (파이프 연결)
19
WmiEventFilter 활동이 감지 됨 (악성코드 실행시 사용될 가능성 있음)
20
WmiEventConsumer 활동이 감지 됨
21
WmiEventConsumerToFilter 활동이 감지 됨
255
Sysmon 오류
[표 1] Sysmon Event ID
ELK + Beats
ELK란 Elasticsearch, Logstash, Kibana의 3가지 오픈소스를 부르는 말이다.
Elasticsarch : 분산 검색 엔진, 데이터 저장
Logstash : 입력 값 필터링
Kibana : 시각화 도구
Beats : 시스템 정보 및 파일 로그 전달 등의 기능
위에 4가지 도구를 이용하여 Sysmon 모니터링 도구를 만들고자 한다. 기본적으로 ELK를 설치하는 방법은 지난 포스팅에서 설명하였기 때문에 자세한 ELK 설치 방법에 대해서는 따로 언급하지 않고 설정과 관련된 부분을 다루려고 한다. 설치와 관련된 정보는 ELK 스택의 사이트(https://www.elastic.co)에서 얻을 수 있다.
전체적인 구성도는 Sysmon을 설치한 파일에 Winlogbeats를 설치하고 해당 데이터를 ELK가 설치된 서버로 전송하는 방식이다. 각각 Logstash는 5044포트로 데이터를 받아 Elasticsearch로 보내고 Kibana의 5601포트로 결과값을 확인 가능하다.
[그림 4] Sysmon + ELK 구성도
Beats 설정
Beats는 경량 로그 수신기로 여러 종류가 있다. 이 중 Winlogbeats는 Eventlog를 전달해주는 역할을 한다. Sysmon을 전달하기 위해서 Sysmon이 설정된 시스템에 Winlogbeats를 설치하고 다음과 같이 설정 파일을 변경해준다. 설정 파일의 내용은 다음과 같다. (winlogbeat.yml)
[그림 5] Winlogbeat.yml
아래와 같은 명령어로 실행이 가능하며 ELK 사이트에서 서비스로 등록하는 방식을 참고하면 서비스로 등록하여 사용이 가능하다.
Beats로 전달 받은 데이터를 더욱 효율적으로 보기 위해 Logstash를 이용하여 각각의 값에 항목을 지정해 준다. Logstash의 필터링은 Logstash의 conf 파일을 수정하여 만들 수 있다.
Sysmon의 데이터 중에 의미있는 값을 선별하게 위해 conf 파일을 작성하였다. 하지만 이는 사용자의 설정에 따라 변경되어야하며 필터링으로 인해 더 다양한 이벤트를 탐지할 수 있다.
이벤트에 대한 conf파일을 만들기 위해서는 우선 입력되는 데이터에 대하여 확인하는 것이 중요하다.
입력되는 데이터는 다음과 같다.
[그림 6] Winlogbeat 입력값
입력되는 데이터 부분중 특히 message부분에서 의미있는 값을 찾아낼 수 있다. X-pack 설치후 활용이 가능한 Grok Debugger나 웹페이지 http://grokdebug.herokuapp.com/를 이용하여 Grok 표현식을 통해 message 부분에서 특정 부분을 추출해서 사용할 수 있다.
엘라스틱서치는 루씬 검색 기능을 모두 사용해서 데이터를 검색할 수 있도록 풍부한 API를 제공한다. 엘라스틱서치는 그 형식 덕분에 다양한 조합으로 검색 요청을 만들 수 있다. 데이터에 사용할 적절한 필터 조합 쿼리를 찾는데 가장 나은 방법은 시험해보는 것이다. 프로젝트 데이터에서 원하는 가장 적합한 것을 찾기 위해 여러가지 조합을 시도해보는 것이 가장 중요하다.
검색 요청과 검색 요청의 결과가 일반적으로 어떻게 보여질까?
검색 API의 주요 구성 요소 중 하나인 쿼리와 필터 DSL를 알아본다.
필터와 쿼리에 사용하는 가장 일반적인 방법 / 쿼리와 필터의 차이점
엘라스틱서치가 도큐먼트의 점수를 어떻게 계산할까?
REST API 검색 요청은 처음 접속하려고 선택한 노드에 전송되고 검색 요청을 모든 샤드(주 또는 레플리카)로 보낸다. 모든 샤드에서 정렬 및 순위를 매긴 결과로부터 충분한 정보를 수집하면, 오직 반환될 도큐먼트 내용을 담고 있는 샤드만 해당 내용을 반환하도록 요청 받는다. 이런 검색 라우팅 기능은 설정할 수 있는데, “query_then_fetch”라 부르는 기본동작이다.
엘라스틱서치 검색 요청은 JSON 도큐먼트 기반 요청이거나 URL 기반 요청이다. 요청은 서버로 보내지고, 모든 검색 요청이 같은 형식을 따르기 때문에 개별 검색 요청을 변경할 수 있는 구성 요소에 대해 이해하는 것이 도움이 된다.
색인은 두 개의 샤드이고 샤드당 한 개의 복제본으로 구성한다. 도큐먼트를 찾고 점수를 산정한 후 상위 n개의 도큐먼트가 반환된다.
(1) 검색 범위 지정하기
모든 REST 검색 요청은 _search REST 종단점을 사용하고 GET이나 POST 요청 중 하나가 된다. 전체 클러스터를 검색하거나 또는 요청 URL에 색인 이름 또는 타입을 지정해서 범위를 제한할 수 있다.
일단 색인을 하면, 다중 색인을 검색하는 alias도 사용할 수 있다. 이 방식은 접근 가능한 날짜와 시간으로 구성한 이름의 색인을 이용해서 검색할 때 종종 사용한다. 즉, logstash-yyyymmdd 형식의 색인은 하나의 앨리어스로 logstash를 호출하지만 실은 전체 색인을 가리키게 된다. 기본 검색을 할 수도 있고 curl ‘localhost:9200/logstash/_search’처럼 전체 logstash 기반 색인에 한정할 수도 있다. 최고 성능을 내려면, 최소한의 색인과 타입을 사용하도록 쿼리를 제한하는 것이 좋다. 각 검색 요청이 모든 색인 샤드에 전송된다는 사실은, 더 많은 색인은 더 많은 샤드에 검색 요청이 전송된다는 것을 의미한다.
(2) 검색 요청의 기본 구성 요소
검색할 색인을 선택했다면, 검색 요청에 있어 가장 중요한 구성 요소를 설정해야 한다. 구성 요소는 반환할 도큐먼트 개수를 제어하고, 최적의 도큐먼트를 선택하게 하며, 원치 않는 도큐먼트는 결과에서 걸러내도록 한다.
query – 검색 요청에 있어 가장 중요한 요소다. 점수 기반으로 최적의 도큐먼트를 반환하거나 원치 않는 도큐먼트를 걸러내도록 설정한다. 이 구성 요소는 DSL 쿼리와 DSL 필터를 사용해서 구성한다.
size – 반환할 도큐먼트 개수를 의미한다.
from – size와 함께 Pagination에 사용한다. 다음 페이지 10개를 결정하기 위해 엘라스틱서치는 상위 20개를 산출해내야 한다는 점을 기억한다. 결과 집합이 커져서 중간쯤 어딘가의 페이지를 가져오는 것은 비용이 많이 들어가는 요청이 될 것이다.
_source – _source 필드는 어떻게 반환할 것인가를 명시한다. 기본값은 완전한 _source 필드를 반환하는 것이다. _source 설정으로 반환되는 필드를 걸러낼 수 있다. 색인된 도큐먼트가 크고 결과에서 전체 내용이 필요하지는 않을때 사용한다. 이 옵션을 사용하려면, 색인 매핑에서 _source 필드를 비활성화하지 않아야 한다. 필드와 _source간에는 차이가 있다.
sort – 기본 정렬은 도큐먼트 점수에 따른다. 그런데, 점수 계산이 필요없거나 동일 점수의 다수 도큐먼트가 예상된다면, 반환한 도큐먼트 결과로부터 가져올때 sort를 추가해서 원하는대로 순서를 제어할 수 있다.
기본 구성 요소 중 쿼리 구성 요소에 대해 알아본다. match_all 쿼리를 match 쿼리로 변경하고, 필터 DSL에서 쿼리 DSL의 filtered 쿼리를 사용하는 검색 요청으로 변경하는 term 필터를 추가할 수 있다. 그리고 필터와 쿼리의 차이점을 깊게 알아 본다.
다음 예제는 “Hadoop” 단어를 제목으로 포함하는 그룹을 찾기 위해 match 쿼리를 사용했다.
쿼리는 3건의 이벤트를 반환한다. 쿼리를 실행해서 처음 일치하는 것의 점수를 확인한다. 첫 일치는 “Using Hadoop with Elasticsearch”라는 제목을 가진 도큐먼트이다. 이 검색을 H가 대문자인 “Hadoop” 단어로 검색하도록 변경할 수 있다. 결과는 같을 것이다.
필터가 쿼리와 유사하긴 해도 점수 매김과 여러가지 검색 기능 수행을 어떻게 적용하는지가 다르다. 쿼리가 특정 텀으로 점수 계산하는 것과 달리, 검색에서 필터는 “이 도큐먼트가 이 쿼리와 일치를 하는가”하는 단순 이진법의 예 또는 아니요 답변만을 반환한다.
필터는 이런 차이점 때문에 일반 쿼리를 사용하는 것보다 더 빠를 수 있고 캐시할 수도 있다. 필터를 이용해서 검색하는 것은 쿼리를 사용하는 일반 검색과 유사하게 보이지만, 실제는 그렇지 않다. 쿼리는 원래의 쿼리와 필터를 포함하는 “filtered” 맵으로 변경한다. 이 쿼리는 쿼리 DSL에서 filtered query로 부른다. filtered query는 두 개의 구성 요소. 즉, 쿼리와 필터를 포함한다.
여기 “hadoop”으로 일치하는 이벤트를 위한 일반적인 쿼리처럼 필터가 사용되었으나, 필터는 “Hadoop” 단어를 위한 쿼리뿐만 아니라 특정 이벤트로 제한할 때에도 사용한다. 이 특정 필터 부분 내부에, host가 “andy”인 모든 도큐먼트를 찾기 위해 텀 필터가 사용되었다. 여기서 엘라스틱서치는 이 필터와 일치하는 도큐먼트인지 아닌지 보여주는 바이너리 비트 집합인 Bitset을 작성한다.
엘라스틱서치가 비트셋을 만들고 나서는 도큐먼트를 필터링하는데 사용할 수는 있으나 검색의 쿼리 일부분으로 검색하는 것은 아니다. 필터는 점수 계산이 필요한 도큐먼트를 제한한다. 제한된 도큐먼트 집합의 접수는 쿼리 기반으로 계산된다. 이런 이유로, 필터를 추가하는 것은 전체 쿼리를 단일 검색으로 결합하는 것보다 더 빠른다. 어떤 종류의 필터인가에 따라 엘라스틱서치는 비트셋 결과를 캐시할 수 있다. 필터를 다른 검색에 사용해도 비트셋을 다시 계산할 필요가 없다.
그러나 어떤 유형의 필터의 경우, 엘라스틱서치가 다시 사용되지 않을 것으로 판단하거나 비트셋 재생성 비용이 사소하다면 자동으로 캐시하지 않을 수 있다. 캐시하기 어려운 쿼리의 예로 최근 한 시간의 모든 문서로 결과를 제한하는 필터의 경우가 그러하다. 이 쿼리는 이를 실행할 때 매초 변경되어야 하므로 캐시할 이유가 없다. 추가로, 엘라스틱서치는 필터를 캐시해야 할지에 관해 수동으로 지정하는 기능을 제공한다. 이 모든 것은 필터를 가진 검색을 더 빠르게 만든다. 그러므로 가능하다면 쿼리의 일부분을 필터로 만들 필요가 있다.
비록 엘라스틱서치에서 쿼리하는 수많은 방법이 있지만, 어떤 쿼리는 색인에 데이터가 어떻게 저장되었는지에 의존하는 쿼리보다 더 나을 수 있다. 엘라스틱서치가 지원하는 쿼리의 서로 다른 타입을 알아보고, 쿼리에서 어떻게 사용하는지 알아보도록 하겠다. 어떤 쿼리가 데이터에 가장 적합한지 결정할 수 있도록 각각 성능에 대해서도 언급하고, 각 쿼리를 사용할 때의 장단점에 대해서 가늠해보도록 한다.
전체 도큐먼트를 반환하는 match_all 쿼리로 시작하여, 특정 필드에 대해 일치하는 다어로 결과를 제한하는 match 쿼리로, 그리고 특정 필드에 있는 텀을 사용해서 결과를 제한하는 텀 필터가지 사용해봤다. 그리고 query_string을 사용하는 방법도 존재한다. 이 쿼리는 URL 기반 검색에서 사용하게 된다.
MATCH_ALL 쿼리
이 필터는 모든 도큐먼트를 일치하게 한다. 도큐먼트 점수에 관해 신경쓰지 않을때라면, 쿼리 대신 필터 사용이 필요할때 match_all 쿼리가 상당히 유용하다. 또는 검색하는 색인과 타입 사이에서 모든 도큐먼트를 반환하고 싶을때 유용하다. 이 쿼리는 다음과 같은 형태다.
거의 모든 것을 검색하기 때문에 검색엔진을 사용함에도 사용자에게는 유용해 보인다. Match_all 쿼리를 기본으로 사용하면 검색요청도 더 쉽게 만들 수 있다. 그러므로 쿼리 요소는 이 경우 완벽하게 생략할 수 있다.
QUERY_STRING 쿼리
엘라스틱서치 서버를 시작하고 실행할때, query_string 쿼리를 이용하면 쉽게 쿼리 결과를 얻을 수 있다. query_string 검색은 요청 URL로부터 또는 요청 본문을 전송하는 것으로부터 수행할 수 있다. 아래 예제는 “nosql”을 포함하는 도큐먼트를 검색하는데, 이 쿼리는 도큐먼트 한 건을 반환할 것이다.
// query_string 검색은 URL 파라미터처럼 전송
curl -X GET 'localhost:9200/get-together/_search?q=nosql&pretty'// 같은 query_string 검색을 요청 본문처럼 전송
curl -X POST 'http://localhost:9200/get-together/_search?pretty' -d '{
"query": {
"query_string" : {
"query" : "nosql"
}
}
}'
기본적으로, query_string 쿼리는 _all 필드를 검색한다. _all 필드는 모든 필드 결합으로 구성되어 있다. description:nosql 처럼 쿼리로 특정 필드를 지정하거나 요청 본문에 default_field를 지정하면 이를 변경할 수 있다.
curl -X POST 'localhost:9200/_search' -d '{
"query": {
"query_string" : {
// 쿼리에 지정된 필드가 없기 때문에 기본 필드가 사용.
"default_field": "description",
"query": "nosql"
}
}
}'
추측할 수 있듯이, 이 문법은 단일 단어를 검색하는 것보다 더 많은 것을 제공한다. 내부적으로, 이는 모두 AND와 OR처럼 boolean 연산자로 서로 다른 텀을 검색하고 추가적으로 마이너스(-) 연산을 사용해서 결과로부터 도큐먼트를 제외하는 결합의 루씬 쿼리 문법이다. 다음 쿼리는 “nosql” 이름으로 모든 그룹을 검색하지만 description에 “mongodb”를 포함하지 않는다.
name:nosql AND -description:mongodb
1999년부터 2001년 사이에 생성한 모든 search와 모든 lucene 그룹은 다음과 같이 검색할 수 있다.
(tags:search OR tags:lucene) AND created_on:[1999-01-01 TO 2001-01-01]
query_string는 쿼리가 위력적이여서, 웹사이트 사용자에게 그대로 노출하면, 엘라스틱서치 클러스터가 위험할 수 있다. 사용자가 잘못된 형식으로 쿼리를 실행하면 예외 상황에 맞닥뜨리게 될 수 있다. 또한, 모든 것을 반환하는 조합을 만들 가능성도 있어서, 이는 클러스터에 리스크가 될 수 있다.
도큐먼트의 필드와 필드 내에 있는 문자열 검색을 허용하는 term, terms, match, multi_match 쿼리를 포함하는 query_string 쿼리는 다른 대체재로 교체하는 것을 추천한다. 적절한 대체재는 simple-query-string 쿼리인데, +,-,AND,OR를 사용하여 쉽게 접근 가능한 쿼리 문법으로 교체할 수 있다.
텀 쿼리와 텀 필터
텀 쿼리와 필터는 실행 가능한 가장 단순한 쿼리인데, 필드와 텀을 지정해서 도큐먼트 내에서 검색할 수 있다. 검색한 텀이 분석되지 않았기 때문에 완전히 일치하는 도큐먼트 결과만 찾는것은 것을 알아둔다. 엘라스틱서치에 의해 어떻게 텍스트를 개별적인 색인 조각으로 토큰을 만드는 지는 데이터 분석 파트에서 확인할 수 있다. 루씬에 익숙하다면, 텀쿼리가 직접적으로 루씬 TermQuery로 매핑된다는 것은 유용한 정보가 된다.
텀 쿼리처럼 텀 필터는 점수에 영향을 미치지 않고 텀을 포함하는 도큐먼트의 제한된 결과가 필요할 때 사용할 수 있다. 점수를 사용한 이전 예제의 도큐먼트 점수를 다음 예제와 비교하자. 필터가 계산에 방해가 되지도 점수에 영향을 미치지도 않는다는 것을 알게 될 것이다. Match_all 쿼리 대문에 모든 도큐먼트의 점수는 1.0이 된다. 도큐먼트 점수는 필터가 쿼리대신 사용되었으므로 이제 상수가 된다.
매치 쿼리는 몇몇 서로 다른 방식으로 동작할 수 있다. 가장 주요한 두 가지의 기능은 boolean과 phrase다.
(1) BOOLEAN 쿼리
기본적으로, 쿼리 매치는 Boolean 기능과 OR 연산을 사용한다. 예를 들어 “Elasticsearch Denver” 텍스트를 검색한다면 엘라스틱서치는 “Elasticsearch OR Denver”로 검색해서 “Elasticsearch Amsterdam”과 “Denver Clojure Group” 모두로부터 get-together groups가 일치할 것이다.
“Elasticsearch”와 “Denver”를 모두 포함하는 결과를 검색하려면, 맵과 추가하는 연산자 필드 설정에 들어가는 일치 필드 이름을 수정하면서 연산자를 변경한다.
도큐먼트 내에서 각 단어의 위치에 따른 leeway 값처럼 특정 구를 검색할때 phrase 쿼리가 유용하다. Leeway는 slop으로 불리는데 구문 내에서 토큰 사이의 거리를 표현하는 수치 값이다. get-together의 그룹 name을 기억해 내려 할때, “Enterprise” 단어와 “London” 단어는 기억하나 name의 나머지는 기억하지 못한다고 하자. “enterprise london” 구문을 기본값 0 대신 slop을 1 또는 2로 설정해서 완전한 그룹 제목을 몰라도 구문을 포함하는 결과를 찾아 검색할 수 있다.
curl 'localhost:9200/get-together/group/_search' -d '{
"query": {
"match": {
"name": {
"type": "phrase" // 일반 match 쿼리 대신 match phrase 쿼리 사용
"query": "enterprise london",
"slop": 1 // slop에 1을 지정해서 텀간 leeway 거리를 가지도록.
}
}
}
}'
Phrase_prefix 쿼리
Match_phrase 쿼리와 유사하게 match_phrase_prefix 쿼리는 phrase로 검색하는 것에 더해서 phrase 내에서 마지막 텀에 프리픽스 매칭을 허용한다. 이 기능은 검색을 제안하는 검색창에서 사용자가 검색 텀을 입력하는 동안 자동완성을 제공하는 데 정말 유용하다. 이런 유형의 기능으로 검색을 사용할 때, max_expansions 설정을 사용해서, 검색이 반환할 이해 가능한 수준의 총 소요 시간 내에서 확장할 프리픽스의 최댓값을 설정하는 것이 좋다.
Boolean과 phrase 쿼리는 사용자 입력을 받아들이는 데 훌륭한 선택이 된다. Query_string 쿼리와 다르게 매치 쿼리가 +,-,ㅡ?,! 같은 예약된 문자를 막지 않으면서 오류 발생이 가장 적은 방식으로 사용자 입력을 그대로 넘기도록 허용하기 때문이다.
(1) MULTI_MATCH로 다중 필드 일치
비록 multi_match 쿼리가 텀즈 쿼리처럼 필드에서 다중 일치로 검색하는 기능으로 고려해볼 수 있겠지만, 이 기능은 다소 다르다. 다만, 다중 필드의 값을 검색하도록 허용한다. 이는 get-together 예제에서 그룹 name과 description 문자열로 검색하기를 원하는 곳에 유용할 수 있다.
매치 쿼리가 phrase 쿼리, prefix 쿼리 또는 phrase_prefix 쿼리로 전환할 수 있는 것처럼, multi_match 쿼리는 type 키를 추가해서 phrase 쿼리 또는 phrase_prefix 쿼리로 전환할 수 있다. Multi_match 쿼리는 검색을 위해 단일 필드 대신 다중 필드를 지정할 수 있다는 것을 제외하고는 매치 쿼리와 완전히 비슷하다.
매치 쿼리를 사용하면 어떠한 것도 검색할 수 있는데, 이는 대부분의 경우 기본적으로 사용할 수 있는 쿼리 타입이라 할 수 있어, 가능한 이를 사용하도록 강하게 추천한다.
bool 쿼리
Bool 쿼리는 몇 개의 쿼리라도 특정 부분이 must, should, must_not로 데이터가 일치하는지 명시하게 하는 쿼리 구문을 이용해서 단일 쿼리에 결합할 수 있다.
bool 쿼리로 must 매치를 지정한다면, 쿼리가 반환하는 오직 일치하는 결과만 받는다.
should 매치를 지정하면, 도큐먼트가 반환하는 특정 개수의 구문이 일치해야 한다는 의미다.
must 구문이 지정되지 않으면, 도큐먼트로부터 반환되는 것으로부터 일치하는 적어도 하나는 가져야 한다.
마지막으로 must_not 구문은 일치하는 도큐먼트가 결과셋에서 제외하는데 사용한다.
bool 쿼리 절
이진 연산 대응
의미
must
다중 절을 결합하기 위해 이진 and 연산을 사용 (query1 AND query2 AND query3)
must 절에서 검색은 도큐먼트가 일치해야 한다. 소문자 and는 함수이고, 대문자 AND는 연산자이다.
must_not
이진 not 연산으로 다중 절 결합
must_not 절에서 검색은 도큐먼트의 부분이 되지 않아야 한다. 다중 절은 이진 not 메소드에서 결합한다.
should
이진 or 연산으로 다중 절 결합
(query1 OR query2 OR query3)
should 절에서 검색은 이진 OR(query1 OR query2 OR query3)와 유사하게 도큐먼트가 일치하거나 그렇지 않을 수 있다. 그러나 최소 minimum_should_match 파라미터로 이 갯수만큼을 일치해야 한다. (must가 존재하지 않으면 기본값은 1이고, must가 존재한다면 0이다)
David가 참성한(attendees 텀) 이벤트를 검색하는데, Clint나 Andy 중 하나는 참석해야 하고, June 30, 2013보다 오래되지 않아야 한다.
query_string과 매치 쿼리같은 범용 쿼리는 검색차에서 사용자가 입력한 단어로 그런 쿼리를 실행할 수 있어서 특히 유용하다. 검색 범위를 좁히는 목적으로 특정 사용자 인터페이스는 검색창 옆에 최근 생성된 그룹을 검색하도록 하는 달력 위젯이나 특정 위치에서 발생한 이벤트를 필터링하려는 체크 상자 같은 또 다른 요소를 포함하기도 한다.
(1) 범위 쿼리와 필터
특정 범위 사이의 숫자, 날짜 및 문자열까지고 사용할 수 이쓴 값으로 쿼리하는 데 사용한다. 범위 쿼리를 사용해서 필드의 상위 및 하위 값을 지정한다.
범위 쿼리는 문자열 범위도 지원한다. 범위 검색을 사용할 때 필터가 더 나은 선택인지 신중하게 생각하는게 좋다. 바이너리 매치를 가지는 범위 쿼리로 들어간 도큐먼트는 범위 쿼리가 되는데 필요하지 않기 때문이다. 쿼리로 만들지 필터로 만들지 확실치 않다면 필터로 만드는 것을 권장한다. 범위 쿼리를 만드는데 99% 경우, 필터가 옳은 선택이다.
프리픽스 쿼리와 필터
텀 쿼리와 유사하게 프리픽스 쿼리와 필터는 검색하기 전에 분석이 안된 프리픽스를 포함하는 텀을 검색하도록 해준다. 예를 들어, “liber”로 시작하는 모든 이벤트의 색인 검색은 다음 쿼리를 사용한다.
검색 프리픽스는 전송 전에는 분석되지 않으므로, “liber”를 “Liber”로 변경하여 검색요청을 보내면 색인에 소문자로 된 텀을 찾을 수 없다. 이는 엘라스틱서치가 도큐먼트를 분석하고 쿼리하는 방식때문이다. 이 기능때문에 프리픽스 쿼리는 텀이 색인의 일부분일때 사용자가 입력한 부분 텀의 자동완선에 있어서 좋은 선택이다. 예를 들어, 존재하는 카테고리가 이미 알려져 있을때 카테고리 입력창을 제공할 수있다. 사용자가 색인의 일부분인 텀을 입력하고 있다면, 사용자의 검색창에 입력하는 텍스트를 소문자로 변경한 다음 결과를 보여주는 어떤 다른 결과를 보여주기 위해 프리픽스 쿼리를 사용할 수 있다.
프리픽스 쿼리로부터 매칭 결과를 가지고 있을때 사용자가 입력하는 도중에 제안처럼 그들을 제공할 수 있다. 그러나 결과에서 텀을 분석하거나 fuzziness 값을 지정하기를 원하다면, 자동완성 기능을 위해 match_phrase_prefix 쿼리를 사용하는 게 아마 나을 것이다.