Search
moon
sun

데이터 다루기

목차

데이터 다루기

•
인덱스에 실제로 문서를 색인, 조회, 업데이트, 삭제하는 방법에 대한 학습
•
다양한 검색쿼리, 페이지네이션, 집계에 대해서 학습
•
JVM 서비스 코드에서 ES Client를 이용해 ES API를 호출하는 방법 학습

색인

PUT [인덱스 이름]/_doc/[_id값] POST [인덱스 이름]/_doc PUT [인덱스 이름]/_create/[_id값] CREATE [인덱스 이름]/_create/[_id값]
Bash
복사
•
ID값이 있다면 PUT, 이미 해당 _id값으로 문서가 있다면 덮어씌운다
•
ID값이 없다면 POST, _id는 ES에서 임의 지정
•
_create는 항상 새 문서를 생성하며 _id가 이미 존재하면 덮어쓰기 하지 않고 실패한다.

refresh

•
색인 시 refresh 매개변수를 통해 색인 직후 샤드 refresh를 수행해 즉시 검색 가능 여부를 설정할 수 있다.
•
refresh 속성
◦
true: 색인 직후 문서가 색인된 shard refresh하고 응답 반환
◦
wait_for: 색인 이후 refresh 될 때까지 기다린 후 응답 반환
◦
false(default): refresh 동작을 수행하지 않는다.
•
잦은 refresh는 비용낭비일뿐 아니라 크기가 작은 세그먼트가 많이 생성될 수 있어 검색 성능도 떨어진다.
•
문서 색인 요청 결과가 검색 역색인에 즉시 동시적으로 반영되야 하는 요청을 최소화하도록 설계하자.
•
즉시 확인이 필요한 경우 검색보단 조회 API를 활용하자.
•
대량의 색인이 필요한 경우 bulk API를 사용하자

조회

GET [인덱스 이름]/_doc/[_id값] GET [인덱스 이름]/_source/[_id값]
Bash
복사
•
_doc은 인덱스나 _id와 같은 메타데이터를 같이 조회한다.
•
_source는 문서의 본문만을 반환한다.

조회 - 필터링

•
_source_includes, _source_excludes 옵션으로 필드 필터링을 할 수 있다.
GET my_index2/_source/1?_source_includes=p*,views # 검색 결과 #{ # "views": 1234, # "public": true, # "point": 4.5 #} GET my_index2/_source/1?_source_includes=p*,views&_source_excludes=public # 검색 결과 #{ # "views": 1234, # "point": 4.5 #}
Bash
복사
•
_source_includes 만 사용할때는 _source로 줄여서 사용할 수 있다.

업데이트

POST [인덱스 이름]/_update/[_id값]
Bash
복사
•
doc을 이용해 업데이트하기
POST [인덱스 이름]/_update/[_id값] { "doc": { [업데이트할 내용] } }
Bash
복사
◦
detect_noop 속성을활용하자
▪
변경 요청은 했지만 실제로 변경된게 없는 경우를 noop이라 하는데, 데이터 특성상 noop이 발생하 ㄹ가능성이 없는 경우 noop 설정을 비활성화해서 약간의 성능 향상을 할 수 있다.
◦
doc_as_upsert를 활용하자
▪
SQL의 upsert와 같은 동작을 하도록 한다. default는 false이다.
•
script를 이용해 업데이트하기
put update_test/_doc/1 { "title":"hello world", "views": 35, "created":"2019-01-17T14:05:01.234Z" } POST update_test/_update/1 { "script":{ "source": "ctx._source.views += params.amount", "lang":"painless", "params":{ "amount":1 } }, "scripted_upsert": false } get update_test/_doc/1
Bash
복사
ES 자체 스크립트 painless를 이용해 스크립트를 작성해 업데이트 할 수 있다.

삭제

DELETE [인덱스 이름]/_doc/[_id값]
Bash
복사

복수 문서 API

bulk API

•
여러 색인, 업데이트, 삭제 작업을 한 번의 요청에 담아서 보내는 API
•
JSON이아니라 NDJSON형태로 데이터를 만들어 보낸다.
◦
Content-Type도 application/x-ndjson을 사용해야 한다.
◦
마지막 줄도 줄바꿈 문자 \n으로 끝나야 한다.
POST _bulk {"index":{"_index":"bulk_test","_id":"1"}} {"field1":"value1"} {"delete":{"_index":"bulk_test","_id":"2"}} {"create":{"_index":"bulk_test","_id":"3"}} {"field1":"value3"} {"update":{"_index":"bulk_test","_id":"1"}} {"doc":{"field2":"value2"}} {"index":{"_index":"bulk_test","_id":"4", "routing":"a"}} {"field1":"value4"}
Bash
복사
•
bulk API 는 기본적으로 순서를 보장하지 않는다. 순서를 가지도록 하고 싶을 경우 인덱스, _id, 라우팅 조합을 동일하게 가져간다면 요청들은 모두 동일한 주 샤드로 넘어갈테고 순서대로 수행될 수 있다.

multi get

•
한 번에 여러 문서를 조회할 수 있다.
GET _mget GET [인덱스 이름]/_mget
Bash
복사
GET _mget { "docs": [ { "_index":"bulk_test", "_id":1 }, { "_index": "bulk_test", "_id": 4, "routing":"a" }, { "_index":"my_index2", "_id":"1", "_source": { "include": [ "p*" ], "exclude": [ "point" ] } } ] } GET bulk_test/_mget { "ids": ["1", "3"] }
Bash
복사

update by query

•
검색 쿼리 조건에 맞는 복수 문서를 찾아 업데이트 및 삭제 작업을 수행할 수 있다.
•
script를 통한 업데이트만을 지원한다.
•
검색 결과 문서들에 대해 스냅샷을 찍고 업데이트를 진행하는데, 업데이트 작업 중 스냅샷을 찍웠던 문서에서 다른 요청으로 인해 변경이 생겨 버전 충돌 문제가 생기면 그만두거나 다음 작업으로 넘어가거나 선택할 수 있다.
◦
conflicts 매개변수를 지정해 동작을 지정할 수 있다.
◦
abort (충돌 시 중단), proceed (다음 작업으로 진행)
POST [인덱스 이름]/_update_by_query { "script": { "source": " //..", }, "query": { //... } }
Bash
복사

update by query - 스로틀링

•
update 작업은 운영 중인 서비스에 영향을 줄 수 있기에 스로틀링을 통해 작업 속도를 조정할 수 있다.
◦
클러스터 부하와 서비스 영향을 최소화하는 목적
POST [인덱스 이름]/_update_by_query ?scroll_size=1000 &scroll=1m &requests_per_second=500 { //... }
Bash
복사
◦
scroll_size: 한번에 가져올 업데이트 대상 문서의 갯수
◦
scroll: 검색 조건을 만족한 모든 문서에 대한 상태를 검색 문맥(search context)를 보존할 시간
◦
requests_per_second: 초당 수행할 작업의 갯수
▪
위 설정(1000, 500)기준으로는 ES는 2초마다 스크롤 한 번 분량만큼 업데이트 한다.
▪
1000개에 문서에 대한 업데이트 작업 수행 시간이 0.5초일 경우 작업 수행 후 1.5초를 대기하면 총 2초간 1000개를 업데이트 한 것이 되어 초당 500개의 작업을 수행한 것이 된다.

비동기적 요청과 tasks API

•
update by query 작업을 wait_for_completion 매개변수를 false로 지정해 비동기로 처리할 수 있다.
•
ES에선 작업을 task로 등록하고 task의 id를 응답에 포함시켜 반환한다.
•
클라이언트는 이 taskId를 통해 작업 진행상황을 조회하거나 취소할 수 있다.
POST [인덱스 이름]/_update_by_query?wait_for_completion=false { //... } # 응답 { "task": "Iqb0AujZQViife0-AOlyPg:1650433" } # task 상태 확인 GET .tasks/_doc/[task id] GET _tasks/[task id] # task 작업 취소 POST _tasks/[task id]/_cancel # task 결과 삭제 DELETE .tasks/_doc/[task id]
Bash
복사

슬라이싱

•
스로틀링으로 부하를 줄이는 대신 업데이트 성능을 올려 빠른 시간내에 작업을 끝내고자 할 때 선택
POST [인덱스 이름]/_update_by_query?slices=auto { //... }
Bash
복사
•
기본값은 1로 작업을 병렬로 쪼개지 않는다는 의미
•
auto로 지정하면 보통 지정한 인덱스의 주 샤드 수만큼 지정된다.

delete by query

•
update by query처럼 지정한 검색 쿼리로 삭제할 대상을 지정한 뒤 삭제를 수행한다.
•
동작 방식이나 충돌 처리, 비동기 처리등에 대한 속성은 update by query와 동일하다.
POST [인덱스 이름]/_delete_by_query { "query":{ //... } }
Bash
복사

검색

GET [인덱스 이름]/_search POST [인덱스 이름]/_search GET _search POST _search
Bash
복사
•
index 이름은 하나 이상을 작성할 수 있고 와일드 카드(*)사용도 가능하다.
◦
GET my_index*,analyzer_test*,mapping_test/_search

쿼리 DSL 검색, 쿼리 문자열 검색

•
쿼리 DSL 검색 : query 필드에 원하는 쿼리와 질의를 작성해서 호출할 수 있다.
GET my_index/_search { "query": { "match": { "title": "hello" } } }
Bash
복사
•
쿼리 문자열 검색 : 루씬 쿼리 문자열을 지정하는 방법으로 수행하는 방법
GET my_index/_search?q=title:hello
Bash
복사
◦
기타 문법
문법
설명
예시
질의어만 기술
전체 필드 검색
hello
필드이름:질의어
지정 필드 대상 검색
title:hello title:(hello OR world)
_exists_:필드이름
특정 필드가 존재하는 문서 검색
_exists_:title
필드이름:[시작 to 끝] 필드이름:{시작 to 끝}
범위 검색, 경계값이 없으면 *을 사용한다.
data:[2021-05-03 to 2021-09-03] count:{10 to 20} date:{* to 2021-09-03} count:[20 to *]
질의어 * or ?
와일드 카드를 포함한 쿼리는 매우 느리고 위험(사용 비권장)
title:hello*
AND, OR, NOT, ( )
쿼리에 연산 적용, ( ) 로 연산 순서 지정
(title:(hello OR world)) AND (contents: NOT bye) OR count: [1 to 3]

match 쿼리

•
지정한 필드의 내용이 질의어와 매치되는 문서를 찾는 쿼리
•
필드가 text라면 필드의 값과 질의어 모두 애널라이저로 분석된다.
GET my_index/_search { "query": { "match": { "fieldName": { "query": "test query sentence" } } } }
Bash
복사
•
fieldName 필드가 text타입이고 기본 애널라이저(standard)라면 질의어(test query sentence)는 test, query, sentence 총 3개의 토큰으로 분석될 것이다.
•
default는 OR 검색이다. AND로 바꾸고 싶다면 operator 속성을 and로 지정하면 된다.

term 쿼리

•
지정한 필드의 값이 질의어와 일치하는 문서를 찾는 쿼리
GET my_index/_search { "query": { "term": { "fieldName": { "value": "hello" } } } }
Bash
복사
•
지정할 필드가 하나 이상이라면 term대신 terms를 사용하면 된다.
GET my_index/_search { "query": { "terms": { "fieldName": ["hello", "world"] } } }
Bash
복사

range 쿼리

•
범위 검색 쿼리
GET [인덱스 이름]/_search { "query": { "range": { "fieldName": { "gte": 100, "lt": 200 } } } }
Bash
복사
•
gt(greater than) lt(less than) gte(greater than or equal to) lte(less than or equal to)
•
역시 부하가 크기에 부담이 없는 상황에서만 사용해야 한다.

prefix 쿼리

•
필드의 값이 지정한 질의어로 시작하는 문서 검색
GET [인덱스 이름]/_search { "query": { "prefix": { "fieldName": { "value": "hello" } } } }
Bash
복사
•
무거운 쿼리로 분류되며 단발성 쿼리로 가끔 수행한다면 괜찮지만 일상 쿼리로는 적절치 않다.
•
매핑에 index_prefixs 설정을 추가해 길이 제한을 해서 성능을 높힐 수 있다.
PUT prefix_mapping_test { "mappings": { "properties": { "prefixField": { "type": "text" "index_prefixes": { "min_chars":3, "max_chars":5, } } } } }
Bash
복사

exists 쿼리

•
필드가 존재하는 문서를 검색한다.
GET [인덱스 이름]/_search { "query": { "exists": { "field": "fieldName" } } }
Bash
복사

bool 쿼리

•
쿼리 조합 검색 쿼리
GET _search { "query": { "bool": { "must": [ {"term": {"field1": {"value": "hello"}}}, {"term": {"field2": {"value": "world"}}} ], "must_not": [ {"term": {"field4": {"value": "elasticsearch-test"}}} ], "filter":[ {"term": {"field3": {"value": true}}} ], "should": [ {"match": {"field4": {"query": "elasticsearch"}}}, {"match": {"field5": {"query": "lucene"}}} ], "minimum_should_match": 1 } } }
Bash
복사
•
must, must_not, filter, shoud 조건절을 이용해 다른 쿼리와 조합할 수 있다.
◦
must, filter 조건절에 들어간 하위 쿼리는 AND 조건으로 만족해야 한다.
◦
must_not 조건절을 만족하는 쿼리는 결과에서 제외된다.
◦
should 조건절에 들어간 쿼리는 minimum_should_match 에 지정한 개수 이상의 하위 쿼리를 만족하는 문서가 포함된다.

검색 결과 정렬

•
검색 요청 payload에 sort를 지정해 검색 결과 정렬이 가능하다.
GET [인덱스 이름]/_search { "query": { ... }, "sort": [ {"field1": { "order":"desc"} }, {"field2": { "order":"asc"} }, "field3" } }
Bash
복사
•
정렬 필드는 여럿 지정이 가능하며 필드명만 지정해도 내림차순 정렬을 한다.
•
text 타입은 정렬대상으로 하지 않도록 하자.
•
_score를 정렬 순서에 포함시켜 유사도 정렬도 가능하다.
•
_doc을 정렬 순서에 포함시켜 문서 번호로 정렬할 수 있다.

페이지네이션

•
from과 size
GET [인덱스 이름]/_search { "from": 10, # 11번째부터 (default: 0) "size": 5, # 16번째까지 (default: 10) "query": { //... } }
Bash
복사
◦
from 15 / size 5로 지정하면, 20개의 문서를 검색한 뒤 마지막 5개를 잘라서 반환한다.
◦
from절이 커질수록 검색해야하는 범위가 커진다. (무거워진다.)
◦
다음 페이지를 검색하는사이에 새로운 값이 추가되거나 삭제될 수 있어 일관성이 떨어진다.
◦
정확한 페이지네이션을 위해서는 from/size는 권장하지 않는다.
◦
ES에선 from + size가 1만이 넘는 검색은 수행이 거부 된다.
•
scroll
: 검색 조건에 매칭되는 전체 문서 순회시 사용하는 방법으로 스크롤 순회도중에는 최초 검색시의 문맥(search context)가 유지된다.
GET [인덱스 이름]/_search?scroll=1m { "size": 1000, "query": { //... } }
Bash
복사
◦
scroll을 사용하면 검색 결과에서 scroll_id를 확인할 수 있는데, 두 번째 검색부터는 이 scroll_id를 지정해서 scroll 검색을 수행하면 된다.
GET [인덱스 이름]/_search/scroll { "scroll_id": "FGluY2x1ZGVfY29....", "scroll": "1m" }
Bash
복사
◦
scroll 검색을 수행할 때마다 검색 문맥은 연장된다.
◦
검색 문맥이 유지되고 있지만 더 빠르게 자원반납을 하고 싶다면 delete로 제거해줄수도 있다.
◦
scroll은 보통 정렬 여부가 상관없는 경우에 사용하기에 정렬을 _doc으로 지정하는게 좋다.
▪
유사도 점수 계산도 하지 않고 정렬을 위한 별도의 자원도 사용하지 않기 때문
GET [인덱스 이름]/_search?scroll=1m { "size": 1000, "query": { //... }, "sort":["_doc"] }
Bash
복사
◦
search_after
▪
서비스에서 사용자에게 제공하는 페이지네이션은 search_after를 사용하는게 좋다.
▪
sort를 지정하며 동일한 정렬 값이 나오지 않게 1개 이상의 동점 제거(tiebreaker)용 필드를 지정해야 한다.
GET kibana_sample_data_ecommerce/_search { "size":20, "query": { "term": { "currency": { "value":"EUR" } } }, "search_after": [1706992790000,"591924"], "sort": [ { "order_date":"desc" }, { "order_id":"asc" } ] } # response { ... "sort": [1706992790000,"591924"] ... }
Bash
복사
▪
동점 제거용 필드는 문서를 고유하게 특정할 수 있는 값이 들어가야 한다.
•
id는 doc_values가 꺼져있기에 비권장
•
차라리 id와 동일한 값을 별도로 필드에 저장해서 동점제거용으로 쓰는게 낫다.
▪
인덱스 상태가 변하는 도중 페이지네이션은 문서 누락이 발생할 수 있기에point in time API를 조합해 사용한다.
◦
search_after - point in time API
▪
keep_alive 매개변수로 지정한 시간만큼 검색 대상의 상태를 고정한다.
POST kibana_sample_data_ecommerce/_pit?keep_alive=1m # response { "id": "gcSHBAEca2liYW5hX3NhbXBsZV9kYXRhX2Vjb21tZXJjZRYzVktwbzJBU1M0V3JTd29remI0YWRBABZYVWFUTmlwOVE3ZTV2NlRlRmpPZnFnAAAAAAAAAXuDFjJhbTVpOXhOUXphUk5XZTI2RXZya2cAARYzVktwbzJBU1M0V3JTd29remI0YWRBAAA=" }
Bash
복사
이 pit id를 search_after에 활용할 수 있다.
GET _search { "size":20, "query": { "term": { "currency": { "value":"EUR" } } }, "pit": { "id": "gcSHBAEca2liYW5hX3NhbXBsZV9kYXRhX2Vjb21tZXJjZRYzVktwbzJBU1M0V3JTd29remI0YWRBABZYVWFUTmlwOVE3ZTV2NlRlRmpPZnFnAAAAAAAAAXuDFjJhbTVpOXhOUXphUk5XZTI2RXZya2cAARYzVktwbzJBU1M0V3JTd29remI0YWRBAAA", "keep_alive": "1m" }, "search_after": [1706992790000,"591924"], "sort": [ { "order_date":"desc" } ] }
Bash
복사
▪
pit id로 검색 대상이 지정되서 인덱스 지정이 불필요하다.

집계

•
aggs 속성을 이용해 집계를 진행할 수 있다.
: 집계에서는 모든 문서가 사용되기에 size 속성은 0으로 설정하도록 하자.
GET kibana_sample_data_ecommerce/_search { "size":0, "query": { "term": { "currency": { "value":"EUR" } } }, "aggs": { "my-sum-aggregation-name": { "sum": { "field": "taxless_total_price" } } } }
Bash
복사
# response { ... "aggregations": { "my-sum-aggregation-name": { "value": 350884.12890625 } } }
Bash
복사
•
집계는 검색 쿼리에 매칭되는 모든 문서를 대상으로 하기에 과도한 집계는 성능 저하를 유발한다.
•
ES 집계에는 메트릭, 버킷, 파이프라인 집계로 분류된다.

집계 - 메트릭 집계

•
avg, max, min, sum 집계로 대상 필드의 집계를 구할 수 있다.
•
stats 집계로 대상 필드의 평균,최댓값,최솟값,합,개수를 모두 계산할 수 있다. (한번에)
•
cadinality 집계로 해당 필드의 카디널리티를 집계할 수 있다.
◦
precision_threshold(default: 3000) 속성을 추가해 정확도를 조절할 수 있다.
◦
정확도를 올릴 수록 메모리를 사용하기에 최종 카디널리티보다 높은 수준으로 유지하는게 좋다.

집계 - 버킷 집계

•
문서를 특정 기준으로 분리해 부분집하으로 만들고, 이에 대해 하위 집계(sub-aggregation)을 수행한다.
•
range 집계로 원하는 버킷 구간을 직접 지정할 수 있다.
{ ... "aggs": { "distance-kilometers-range": { "range": { "field": "DistanceKilometers", "ranges": [ { "to": 5000 }, { "from": 5000, "to", 10000 }, { "from": 10000 } ] } }, "aggs": { "average-ticket-price": { "avg": { "field": "AvgTicketPrice" } } } }
Bash
복사
◦
각각의 버킷 내에서 AvgTicketPrice라는 필드의 평균값을 구한다.
•
data_range 집계로 date 타입에 대한 집계도 가능하다.
◦
시간 계산식을 사용할 수 있다.
◦
now와 같이 호출시점에 값이 달라지는 집계 요청은 샤드 요청 캐시에 캐싱되지 않는다.
•
histogram 집계는 범위의 시작-끝이 아니라 간격을 지정해서 버킷을 만들어 집계한다.
◦
지정 필드의 최소값-최대값을 구한뒤 interval 간격으로 버킷을 분리한다.
◦
offset을 이용해 위치 조정도 가능하다.
GET _search { "size":0, "query": { "match_all": {}}, "aggs": { "my-histogram": { "histogram": { "field": "DistanceKilometers", "interval": 1000 } } } } # response "aggregations": { "my-histogram": { "buckets": [ { "key": 0, "doc_count": 1799 }, { "key": 1000, "doc_count": 1148 }, { "key": 2000, "doc_count": 529 }, ... } }
Bash
복사
GET _search { "size":0, "query": { "match_all": {}}, "aggs": { "my-histogram": { "histogram": { "field": "DistanceKilometers", "interval": 1000, "offset": 50 } } } } # response "aggregations": { "my-histogram": { "buckets": [ { "key": -950, "doc_count": 641 }, { "key": 50, "doc_count": 1226 }, { "key": 1050, "doc_count": 1106 }, ... ] } }
Bash
복사
offset이 50이면 [0, 50] 구간이 포함될 버킷이 필요해 [-950, 50] 구간이 생긴다.
•
date 타입도 data_histogram으로 버킷 집계가 가능하다.
◦
구간 설정 속성은 interval이 아닌 calendar_interva | fixed_interval을 사용한다.
•
terms 집계로 지정 필드에 대해 가장 빈도수가 높은 term 순서대로 버킷을 생성한다.
◦
size속성으로 버킷 생성 갯수를 지정한다.
GET kibana_sample_data_logs/_search { "size": 0, "query": { "match_all": {}}, "aggs": { "my-terms-aggs": { "terms": { "field": "host.keyword", "size": 10 } } } } # response { ... "aggregations": { "my-terms-aggs": { "doc_count_error_upper_bound": 0, #doc_count의 오차 상한선 "sum_other_doc_count": 0, # 버킷에 포함되지 않은 문서 수 "buckets": [ { "key": "artifacts.elastic.co", "doc_count": 6488 }, { "key": "www.elastic.co", "doc_count": 4779 }, { "key": "cdn.elastic-elastic-elastic.org", "doc_count": 2255 }, { "key": "elastic-elastic-elastic.org", "doc_count": 552 } ] } } }
Bash
복사
•
모든 term에 대한 페이지네이션으로 전체 순회하며 집계를 하고 싶다면 composite 집계 사용이 좋다.
◦
composite 집계는 sources로 지정된 하위 집계 버킷 전부 페이지네이션을 이용해 순회하는 집계
GET kibana_sample_data_logs/_search { "size": 0, "query": { "match_all": {}}, "aggs": { "composite_aggs": { "composite": { "size": 100, # 페이지네이션에서 한 번에 반환할 버킷 갯수 "sources": [ { "terms-aggs": { "terms": { "field": "host.keyword" } } }, { "data-histogram-aggs": { "date_histogram": { "field": "@timestamp", "calendar_interval": "day" } } } ] } } } }
Bash
복사

집계 - 파이프라인 집계

•
집계 결과를 집계 대상으로 수행한다.
•
buckets_path 속성으로 다른 집계의 결과를 가져오며 상대 경로로 지정된다.
◦
>: 하위 집계로 이동하는 구분자
◦
.: 하위 메트릭으로 이동하는 구분자
◦
집계 이름
◦
메트릭 이름
•
cumulative_sum 집계로 다른 집계의 값을 누적해 합산한다.
GET kibana_sample_data_ecommerce/_search { "size": 0, "query": { "match_all": {}}, "aggs": { "daily-timestamp-bucket": { "date_histogram": { "field":"order_date", "calendar_interval": "day" }, "aggs": { "daily-total-quantity-average": { "avg": { "field": "total_quantlty" } }, "pipeline-sum": { "cumulative_sum": { "buckets_path": "daily-total-quantity-average" } } } } } }
Bash
복사
•
max_bucket 집계로 가장 값이 큰 버킷의 key와 결과값을 구할 수 있다.
GET kibana_sample_data_ecommerce/_search { "size": 0, "query": { "match_all": {}}, "aggs": { "daily-timestamp-bucket": { "date_histogram": { "field":"order_date", "calendar_interval": "day" }, "aggs": { "daily-total-quantity-average": { "avg": { "field": "total_quantity" } } } }, "max-total-quantity": { "max_bucket": { "buckets_path": "daily-timestamp-bucket>daily-total-quantity-average" } } } } # response { ... "max-total-quantity": { "value": 2.289473684210526, "keys": [ "2024-02-02T00:00:00.000Z" ] } }
Bash
복사

ES Client 사용하기

•
JVM용 클라이언트
◦
저수준 REST 클라이언트
▪
HTTP 기반 ES 통신 클라이언트
▪
Request, Response 데이터에 대한 가공 작업은 사용자가 직접 해야 한다.
◦
고수준 REST 클라이언트 | 자바 클라이언트
▪
둘 다 저수준 REST 클라이언트를 래핑해서 만들었다.
▪
고수준 클라이언트는 7.15부터 지원 중단
▪
자바클라이언트는 개발초기라서 고수준 클라이언트와 혼용해서 사용한다.
📌
즉, ES 8이상이라면 그냥 자바 클라이언트를 사용하도록 하자
1.
의존성 추가
implementation 'com.fasterxml.jackson.core:jackson-databind:2.16.1' implementation 'co.elastic.clients:elasticsearch-java:8.11.4' implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.16.1' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.1'
Groovy
복사
•
jackson-databind : 자바 객체 to JSON 직렬화 및 역직렬화 라이브러리
•
elasticsearch-java: ES 자바 클라이언트 라이브러리
•
jackson-module-kotlin: jackson 코틀린 지원 라이브러리
•
jackson-datatype-jsr310: ZonedDateTime, LocalDateTime과 같은 JSR-310 날짜 객체 처리용 라이브러리
2.
클라이언트 초기화
private fun getEsClient(): ElasticsearchClient { // Elasticsearch REST 클라이언트 빌더 생성 val restClientBuilder = RestClient.builder( HttpHost("localhost", 9200, "http") ) // 빌더를 사용하여 낮은 수준의 REST 클라이언트 인스턴스 생성 val lowLevelRestClient = restClientBuilder.build() // Jackson JSON 매퍼 구성. Java 8 날짜/시간 API 모듈 추가 및 몇 가지 옵션 설정 val mapper = jacksonMapperBuilder() .addModule(JavaTimeModule()) // Java 8 날짜/시간 모듈 추가 .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) // 날짜를 타임스탬프로 쓰지 않도록 설정 .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) // 알 수 없는 속성에 대해 실패하지 않도록 설정 .build() // Jackson JSON 매퍼를 사용하여 REST 클라이언트 트랜스포트 설정 val transport = RestClientTransport(lowLevelRestClient, JacksonJsonpMapper(mapper)) // 최종적으로 Elasticsearch 클라이언트 인스턴스 반환 return ElasticsearchClient(transport) }
Kotlin
복사
3.
테스트용 객체 생성
data class MyIndexClass( val fieldOne: String, val fieldTwo: Long, val fieldThree: ZonedDateTime ) data class MyPartialIndexClass( val fieldOne: String )
Kotlin
복사
4.
문서 색인 및 조회해보기
// 색인 private fun indexExample(client: ElasticsearchClient) { val indexResponse = client.index { builder -> builder .index("my-index") //색인 요청 함수 index .id("my-id-1") .routing("my-routing-1") .document(MyIndexClass("hello", 2L, ZonedDateTime.now(ZoneOffset.UTC))) } val result = indexResponse.result() println("result: $result") } // 조회 private fun getExample(client: ElasticsearchClient) { val request = GetRequest.Builder() .index("my-index") .id("my-id-1") .routing("my-routing-1") .build() client.get(request, MyIndexClass::class.java) .run { println("result: $this") } }
Kotlin
복사
5.
bulk API 사용해보기
// Builder Pattern을 사용한 bulk API private fun bulkExampleOne(client: ElasticsearchClient) { val createOperation = CreateOperation.Builder<MyIndexClass>() .index("my-index") .id("my-id-2") .routing("my-routing-2") .document(MyIndexClass("world", 2L, ZonedDateTime.now(ZoneOffset.UTC))) .build() val indexOperation = IndexOperation.Builder<MyIndexClass>() .index("my-index") .id("my-id-3") .routing("my-routing-3") .document(MyIndexClass("world", 4L, ZonedDateTime.now(ZoneOffset.UTC))) .build() val updateAction = UpdateAction.Builder<MyIndexClass, MyPartialIndexClass>() .doc(MyPartialIndexClass("world updated")) .build() val updateOperation = UpdateOperation.Builder<MyIndexClass, MyPartialIndexClass>() .index("my-index") .id("my-id-1") .routing("my-routing-1") .action(updateAction) .build() val bulkOne = BulkOperation.Builder().create(createOperation).build() val bulkTwo = BulkOperation.Builder().index(indexOperation).build() val bulkThree = BulkOperation.Builder().update(updateOperation).build() val operations = listOf(bulkOne, bulkTwo, bulkThree) val response = client.bulk { it.operations(operations) } response.items().forEach { println("result: ${it.result()}, error: ${it.error()}") } } // Functional Call Pattern을 사용한 bulk API private fun bulkExampleTwo(client: ElasticsearchClient) { val bulkResponse = client.bulk { _0 -> _0 .operations { _1 -> _1 .index { _2: IndexOperation.Builder<MyIndexClass> -> _2 .index("my-index") .id("my-id-4") .routing("my-routing-4") .document(MyIndexClass("world", 4L, ZonedDateTime.now(ZoneOffset.UTC))) } } .operations { _1 -> _1 .update { _2: UpdateOperation.Builder<MyIndexClass, MyPartialIndexClass> -> _2 .index("my-index") .id("my-id-2") .routing("my-routing-2") .action { _3 -> _3 .doc(MyPartialIndexClass("world updated")) } } } } bulkResponse.items().forEach { println("result: ${it.result()}, error: ${it.error()}") } }
Kotlin
복사
•
Functional Call Pattern을 사용한 함수에서 _0, _1과 같은 이름을 사용했는데 이는 스칼라 공식 문서에서 람다의 깊이에 따른 네이밍 권장사항이다.
6.
검색 API 사용해보기
private fun searchExample(client: ElasticsearchClient) { val response = client.search({ builder -> builder .index("my-index") .from(0) .size(10) .query { query -> query .term { term -> term .field("fieldOne") .value { value -> value.stringValue("world") } } } }, MyIndexClass::class.java) response.hits().hits().forEach { println("result: $it") } }
Kotlin
복사
7.
BulkIngester 사용하기
: ES 8.7부터 자바 클라이언트에 추가된 BulkIngester는 고수준 REST Client의 BulkProcessor를 대체하는 기능으로 여러 문서에 관련된 작업들을 한 번에 ES로 요청하는 기능
// 대량(bulk) 작업의 생명 주기를 관리하는 리스너 class BulkIngestListener<Context> : BulkListener<Context> { override fun beforeBulk(executionId: Long, request: BulkRequest?, contexts: MutableList<Context>?) { println("[${LocalDateTime.now()}] beforeBulk - executionId: $executionId") } override fun afterBulk( executionId: Long, request: BulkRequest?, contexts: MutableList<Context>?, response: BulkResponse? ) { println( "[${LocalDateTime.now()}] " + "afterBulk - executionId: $executionId, " + "itemSize: [${response?.items()?.size}], " + "took : ${response?.took()}, " + "errors: ${response?.errors()}" ) } // 작업이 완료되기전 혹은 오류가 발생한 후 호출하는 메서드 override fun afterBulk( executionId: Long, request: BulkRequest?, contexts: MutableList<Context>?, failure: Throwable? ) { System.err.println(failure) } } private fun bulkIngesterExample(client: ElasticsearchClient) { val listener = BulkIngestListener<String>() val ingester = BulkIngester.of<String> { it.client(client) // Elasticsearch 클라이언트를 설정합니다. .maxOperations(200) // 대량 작업에서 처리할 최대 작업 수를 설정합니다. 이 값이 너무 낮으면 오버헤드가 증가하고, 너무 높으면 메모리 문제가 발생할 수 있습니다 .maxConcurrentRequests(1) // 동시에 처리할 최대 요청 수를 설정합니다. 이 값은 Elasticsearch 클러스터의 용량과 원하는 병렬 수준에 따라 설정해야 합니다 .maxSize(5242880L) // 대량 요청의 최대 크기를 바이트 단위로 설정합니다. 이 값은 사용 가능한 메모리와 문서의 크기에 따라 설정해야 합니다 .flushInterval(5L, TimeUnit.SECONDS) // 대량 요청이 실행되는 간격을 설정합니다. 이 간격은 작업 수나 요청 크기에 상관없이 적용됩니다 .listener(listener) // 대량 작업의 생명주기를 관리하는 리스너를 설정합니다. } for (number in 0L until 1100L) { val bulkOperation = BulkOperation.of { bulkBuilder -> bulkBuilder.index { indexOpBuilder: IndexOperation.Builder<MyIndexClass> -> indexOpBuilder .index("my-index") .id("my-id-$number") .routing("my-routing-$number") .document(MyIndexClass("world", number, ZonedDateTime.now(ZoneOffset.UTC))) } } ingester.add(bulkOperation) } println("[${LocalDateTime.now()}] sleep 10 seconds ...") Thread.sleep(10000L) for (number in 1100L until 1200L) { val bulkOperation = BulkOperation.of { bulkBuilder -> bulkBuilder.index { indexOpBuilder: IndexOperation.Builder<MyIndexClass> -> indexOpBuilder .index("my-index") .id("my-id-$number") .routing("my-routing-$number") .document(MyIndexClass("world", number, ZonedDateTime.now(ZoneOffset.UTC))) } } ingester.add(bulkOperation) } println("[${LocalDateTime.now()}] sleep 10 seconds ...") Thread.sleep(10000L) ingester.close() }
Kotlin
복사
•
.maxOperations(200)
◦
대량 작업에서 처리할 최대 작업 수를 설정합니다.
◦
이 값이 너무 낮으면 오버헤드가 증가하고, 너무 높으면 메모리 문제가 발생할 수 있습니다
•
.maxConcurrentRequests(1)
◦
동시에 처리할 최대 요청 수를 설정합니다.
◦
이 값은 Elasticsearch 클러스터의 용량과 원하는 병렬 수준에 따라 설정해야 합니다
•
.maxSize(5242880L)
◦
대량 요청의 최대 크기를 바이트 단위로 설정합니다.
◦
이 값은 사용 가능한 메모리와 문서의 크기에 따라 설정해야 합니다
•
.flushInterval(5L, TimeUnit.SECONDS)
◦
대량 요청이 실행되는 간격을 설정합니다.
◦
이 간격은 작업 수나 요청 크기에 상관없이 적용됩니다
•
listener(listener)
◦
대량 작업의 생명주기를 관리하는 리스너를 설정합니다.
Kakao Channel