4. 데이터 다루기 -1
4.1 단건 문서 API
- 문서(document) API는 인덱스에 문서를 색인, 조회, 업데이트, 삭제하는 API이다. 상세한 내용을 알아보도록 하자.
4.1.1 색인 API
- 색인 API는 문서 단건을 색인한다.
1
2
3
4
PUT [인덱스 이름]/_doc/[_id값]
POST [인덱스 이름]/_doc
PUT [인덱스 이름]/_create/[_id값]
POST [인덱스 이름]/_create/[_id값]
각 API의 차이
- 기본 API 는
PUT [인덱스 이름]/_doc/[_id값]
이다.- _id값에 일치하는 문서가 이미 있다면 새 문서로 덮어 씌움
- POST 메서드는 _id 값을 지정하지 않고 색인을 요청할 경우에 사용한다.(es에서 random한 _id 생성)
_create
메서드의 경우에는 항상 새 문서를 생성하는 경우에만 허용하고, 기존 문서를 덮어쓰면서 색인하는것을 금지한다.
1
2
3
4
5
6
7
8
### 4.1.1 색인 API
PUT /my_index2/_create/2
Host: localhost:9200
Content-Type: application/json
{
"hello": "world2"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"error": {
"root_cause": [
{
"type": "version_conflict_engine_exception",
"reason": "[2]: version conflict, document already exists (current version [1])",
"index_uuid": "zV7gpn5XQ4aHoQ9Bw36ygA",
"shard": "0",
"index": "my_index2"
}
],
"type": "version_conflict_engine_exception",
"reason": "[2]: version conflict, document already exists (current version [1])",
"index_uuid": "zV7gpn5XQ4aHoQ9Bw36ygA",
"shard": "0",
"index": "my_index2"
},
"status": 409
}
- 한번 생성된 _id로 다시 한번 create 하면 error 응답을 주는것을 확인할 수 있다.
라우팅
- 라우팅을 지정하지 않는 경우 _id 값의 해시값을 기반으로 샤드가 배정된다.
1
2
3
4
5
6
7
8
9
10
### routing
PUT /routing_test/_doc/2?routing=myid2
Host: localhost:9200
Content-Type: application/json
{
"login_id": "myid",
"comment": "hello elasticsearch",
"created_at" : "2022-12-01T00:08:12.378Z"
}
refresh
- 색인시 refresh 매개변수를 지정하면 문서를 색인한 직후에 해당 샤드를 refresh해서 즉시 검색 가능하게 만들 것인지 여부를 지정할 수 있다.
- refresh 공식 문서
- true : 색인 직후 문서가 색인된 샤드를 refresh 하고 응답
- wait_for : 색인 이후 문서가 refresh될 때까지 기다린 후 응답을 반환. true로 지정했을때와 다르게 refresh를 직접 유발하지는 않음.
index.refresh_interval
에 지정된 시간까지 대기하다가 refresh가 완료되면 응답함 - false : 기본값으로 refresh와 관련된 동작을 수행하지 않음.
refresh 관련 주의사항
- 실제 서비스를 만들다보면 색인 직후 검색 API를 사용해야 하고, 그 검새 결과에 최신 변경 내용이 포함되어야하는 경우가 있고 그경우 refresh 옵션을 고려할 수 있다.
- 다만, true 또는 wait_for 옵션을 사용할 경우 반드시 성능에 대한 고려가 필요하다.
- 호출량이 많은 서비스가 매번 refresh를 지정해 호출하면 전체 클러스터 성능이 크게 저하될 수 있다.
- 이러한 사용방식은 너무 많은 작은 세그먼트를 생성하여 장기적으로 검색 성능이 떨어지고, 추후 세그먼트 병합 비용도 발생함.
- wait_for을 하는 경우에도 너무 많은 대기 요청이 있는 경우 강제로 refresh가 수행될 수 있음.
- index.max_refresh_listeners 설정 기본값 : 1000(이 이상 wait_for하는 경우 강제 refresh)
추가 인덱스 설계 코멘트
- 기본적으로 문서 색인 요청의 결과가 검색 역색인에 즉시 동기 반영되어야 하는 케이스는 많지 않도록 서비스 설계해야 함.
- 대량 색인의 경우 단건 색인 API 대신 bulk API를 사용해야 한다.
4.1.2 조회 API
- 문서 단건을 조회하는 API, es refresh 되지 않아도 변경 내용을 확인할 수 있다.
- 고유 식별자를 지정하여 조회하는것이므로 역색인이 필요없고, translog에서도 데이터를 읽어올수 있음.
1
2
GET [인덱스 이름]/_doc/[_id값]
GET [인덱스 이름]/_source/[_id값]
_doc과 _soruce의 차이
- _doc을 이용하면 인덱스, _id를 포함한 기본적인 메타 데이터도 함꼐 조회할 수 있음.
- _soruce를 이용하면 메타데이터 없이 문서 정보만 조회할 수 있음.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
### _doc
GET /my_index2/_doc/2
Host: localhost:9200
Content-Type: application/json
{
"_index": "my_index2",
"_id": "2",
"_version": 1,
"_seq_no": 1,
"_primary_term": 2,
"found": true,
"_source": {
"hello": "world2"
}
}
### _source
GET /my_index2/_source/2
Host: localhost:9200
Content-Type: application/json
{
"hello": "world2"
}
필드 필터링
_source_includes
와_source_excludes
옵션을 사용하면 결과에 원하는 필드만 필터링해 포함시킬 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
### create
PUT /my_index2/_create/3
Host: localhost:9200
Content-Type: application/json
{
"title": "hello world",
"view": 1234,
"public": true,
"point": 4.5,
"created_at" : "2019-01-17T14:05:01.234Z"
}
### 필드 필터링(_source_includes)
GET /my_index2/_doc/3?_source_includes=p*,views,
Host: localhost:9200
Content-Type: application/json
1
2
3
4
5
6
7
8
9
10
11
12
{
"_index": "my_index2",
"_id": "3",
"_version": 1,
"_seq_no": 2,
"_primary_term": 2,
"found": true,
"_source": {
"public": true,
"point": 4.5
}
}
1
2
3
4
### 필드 필터링(_source_includes & _source_excludes)
GET /my_index2/_doc/3?_source_includes=p*,views&_source_excludes=public
Host: localhost:9200
Content-Type: application/json
1
2
3
4
5
6
7
8
9
10
11
12
{
"_index": "my_index2",
"_id": "3",
"_version": 2,
"_seq_no": 3,
"_primary_term": 2,
"found": true,
"_source": {
"views": 1234,
"point": 4.5
}
}
- includes, excludes를 꼭 같이 사용할 필요는 없고, excludes에 명시적으로 지정한 경우는 무조건 처리됨.
- includes만 사용하고자 하는 경우
_source
매개변수로 사용할수 있다.- _source는 true/false를 지정해 문서의 소스를 가져올지 말지에 대해서도 지정할수 있는데 필드명이
false
인 경우 충돌이 있을수 있음.
- _source는 true/false를 지정해 문서의 소스를 가져올지 말지에 대해서도 지정할수 있는데 필드명이
- includes, excludes에 명시한 내용이 적절히 조회되는 것을 알 수 있다.
라우팅
- 조회 API도 색인과 마찬가지로 라우팅을 반드시 제대로 지정해야 한다.
1
2
3
GET /routing_test/_doc/2?routing=myid2
Host: localhost:9200
Content-Type: application/json
추가 검색 옵션들
- https://www.elastic.co/guide/en/elasticsearch/reference/8.11/search-your-data.html
4.1.3 업데이트 API
- 지정한 문서 하나를 업데이트한다.
- 기본적으로 부분 업데이트(partial updarte)로 동작한다.(문서 전체를 교체하려면 색인 API를 사용하라)
1
POST [인덱스 이름]/_update/[_id값]
- API 자체는 부분 업데이트이지만, 루씬 세그먼트가 불변이라서 실제 ES의 업데이트 작업은 기존 문서의 내용을 조회한 뒤 부분 업데이트될 내용을 합쳐 새 문서를 만들어 색인하는 형태로 진행된다.
- 업데이트 API에는 doc을 이용하는 방법과 script를 이용하는 방법이 있다.
doc에 내용을 직접 기술하여 업데이트
1
2
3
4
5
6
POST [인덱스 이름]/_update/[_id값]
{
"doc": {
[업데이트할 내용]
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
### create
PUT /update_test/_doc/1
Host: localhost:9200
Content-Type: application/json
{
"title": "hello world",
"views": 35,
"created_at" : "2019-01-17T14:05:01.234Z"
}
### update
POST /update_test/_update/1
Host: localhost:9200
Content-Type: application/json
{
"doc": {
"views": 36,
"updated_at" : "2019-01-23T17:00:01.567Z"
}
}
### get
GET /update_test/_doc/1
Host: localhost:9200
Content-Type: application/json
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"_index": "update_test",
"_id": "1",
"_version": 2,
"result": "updated",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"_seq_no": 1,
"_primary_term": 1
}
- update 응답에 result : updated로 나오는것을 알 수 있다. 이후에 다시 조회하면 부분 업데이트가 적용된 결과를 확인할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"_index": "update_test",
"_id": "1",
"_version": 2,
"_seq_no": 1,
"_primary_term": 1,
"found": true,
"_source": {
"title": "hello world",
"views": 36,
"created_at": "2019-01-17T14:05:01.234Z",
"updated_at": "2019-01-23T17:00:01.567Z"
}
}
detect_noop
- 업데이트 API를 호출하면 ES는 그 작업을 수행하기 전에 업데이트 내용이 기존문서 내용을 실질적으로 변경하는지 여부를 확인한다.
- 실제 데이터 변경사항이 없으면 요청(noop 요청, no operation)이라면 쓰기 작업을 수행하지 않는다.
- noop 검사를 통해 불필요한 디스크 I/O를 줄일 수 있고, 이 기본값은 true이다.
- detect_noop을 명시적으로 false로 지정하여 비활성화할수 있는데, 이 경우에는 업데이트가 일어난다.
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"_index": "update_test",
"_id": "1",
"_version": 2,
"result": "noop",
"_shards": {
"total": 0,
"successful": 0,
"failed": 0
},
"_seq_no": 1,
"_primary_term": 1
}
detect_noop 옵션을 비활성화하는 상황
- 일반적인 상황에서는 활성화하는 것이 좋다.
- 데이터 특성상 noop 업데이트가 발생할 가능성이 아예 0%인 경우에는 detect_noop을 비활성화하는 것이 성능을 약간 향상시켜줄 수 있다.
detect_noop + custom plugin 작성시 유의사항
- custom plugin 등 ES를 커스터마이징한다면, 이 옵션을 주의 깊게 살펴봐야 한다.
- 플러그인에서 IndexingOperationListener 같은 리스너를 달 경우 색인 전후에 원하는 작업을 끼워넣을 수 있다.
- 업데이트 요청이 noop로 처리된다면 ES입장에서는 실제 색인이 수행되지 않으므로 IndexingOperationListener의 preIndex, postIndex 핸들러쪽이 수행되지 않게 된다.
doc_as_upsert
- 업데이트 API는 기본적으로 기존문서의 내용을 먼저 읽어들인 뒤 업데이트를 수행한다.(기존 문서가 없으면 요청 실패)
- doc_as_upsert 옵션을 통해 기존문서가 없는 경우 새로 문서를 추가하는 방법을 제공한다.
1
2
3
4
5
6
7
8
9
10
11
12
### doc_as_upsert
POST /update_test/_update/2
Host: localhost:9200
Content-Type: application/json
{
"doc": {
"views": 36,
"updated_at" : "2019-01-23T17:00:01.567Z"
},
"doc_as_upsert": true
}
script를 이용한 업데이트
- 별도의 스크립트 언어를 넣어서 문서를 업데이트 하는 방법이 있따.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
### script를 이용하여 업데이트
POST /update_test/_update/1
Host: localhost:9200
Content-Type: application/json
{
"script": {
"source": "ctx._source.views += params.amount",
"lang": "painless",
"params": {
"amount": 1
}
},
"scripted_upsert": false
}
script
: 스크립트를 이용해서 업데이트를 수행할 때는 그 내용을 위와 같이 script 필드 안에 기술한다.source
: 소스는 스키릅트 본문 기술lang
: 스크립트 언어 종류 지정(기본값 : painless)params
: 스크립트 본문에서 사용할 매개변수 값들을 넣어둘수 있다.scripted_upsert
: 스크립트를 사용한 업데이트가 upsert로 동작하도록 할지를 지정(기본값 false)
제공하는 스크립트 langauge
라우팅과 refresh
- 업데이트 API에도 색인 API와 마찬가지로 routing과 refresh 옵션을 지정할 수 있다.
- 색인시 routing을 지정했다면 업데이트시에도 동일한 값으로 라우팅을 지정해 업데이트해야 의도한 대로 동작한다.
- refresh 션 또한 조회 API와 동일하게 동작한다.
4.1.4 삭제 API
- 지정한 문서 하나를 삭제한다.
- 한번 삭제한 문서는 되돌릴 수 없기 때문에 삭제 작업을 항상 신중해야 한다.
- 삭제 API에도 색인이나 업데이트와 마찬가지로 routing과 refresh 옵션을 지정할 수 있다.
1
DELETE [인덱스 이름]/_doc/[_id값]
삭제시 주의사항
DELETE [인덱스 이름]
을 하는 경우 인덱스 전체가 삭제될 수 있다.
4.2 복수 문서 API
- 서비스 환경에서 단건 문서 API보다는 복수 문서 API를 활용해야 한다.
4.2.1 bulk API
- bulk API는 여러 색인, 업데이트, 삭제 작업을 한 번의 요청에 담아서 보내는 API이다.
- bulk API는 ES의 다른 API와는 다르게 요청 본문을 JSON이 아니라 NSJSON 형태로 만들어서 보낸다.
- 요청 Content-Type 헤더에
application/x-ndjson
을 사용해야 한다.- 여러 줄의 JSON을 줄바꿈 문자(
\n
)로 구분하여 요청
- 여러 줄의 JSON을 줄바꿈 문자(
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
### _bulk
### intellij version에 따라 body parse가 안되는 이슈가 있는듯
POST /_bulk
Host: localhost:9200
Content-Type: application/x-ndjson
{"index":{"_index":"bulk_test","_id":"1"}}\n
{"field1":"value1"}\n
{"delete":{"_index":"bulk_test","_id":"2"}}\n
{"create":{"_index":"bulk_test","_id":"3"}}\n
{"field1":"value3"}\n
{"update":{"_index":"bulk_test","_id":"1"}}\n
{"doc":{"field2":"value2"}}\n
{"index":{"_index":"bulk_test","_id":"4","routing":"a"}}\n
{"field1":"value4"}\n
\n
- 위 요청은 5개의 세부 요청으로 이루어져 있고, 아래의 요청이 포함되어 있다.
- index, create : 색인 요청
- create는 문서 생성하는것만 허용하고, 기존 문서를 덮어쓰지 않는다.
- index는 기존에 동일한 _id로 문서가 존재하지는 여부와 상관 없이 항상 색인을 수행한다.
- update : 업데이트 요청
- 다음 라인에서 doc 또는 script 사용 가능.
- delete : 삭제 요청
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
{
"errors": false,
"took": 2180,
"items": [
{
"index": {
"_index": "bulk_test",
"_id": "1",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 1,
"status": 201
}
},
{
"delete": {
"_index": "bulk_test",
"_id": "2",
"_version": 1,
"result": "not_found",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"_seq_no": 1,
"_primary_term": 1,
"status": 404
}
},
{
"create": {
"_index": "bulk_test",
"_id": "3",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"_seq_no": 2,
"_primary_term": 1,
"status": 201
}
},
{
"update": {
"_index": "bulk_test",
"_id": "1",
"_version": 2,
"result": "updated",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"_seq_no": 3,
"_primary_term": 1,
"status": 200
}
},
{
"index": {
"_index": "bulk_test",
"_id": "4",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"_seq_no": 4,
"_primary_term": 1,
"status": 201
}
}
]
}
- bulk API의 응답은 각 세부 요청을 수행하고 난 결과를 모아 하나의 응답으로 돌아온다.
- 전체 응답 내 각 세부 응답은 요청의 순서와 동일하다.
- 각 요청별 세부 응답의 상태코드를 통해 처리결과를 각각 전달받을 수 있다.
특정 index의 bulk 처리
1
POST [인덱스이름]/_bulk
- 위처럼 _bulk 앞에 인덱스 이름을 넣으면 기본 대상이 해당 인덱스로 지정된다.
bulk API의 작업 순서
- bulk API에 기술된 작업은 반드시 그 순서대로 수행된다는 보장이 없다.
- 조정 역할을 하는 노드가 요청을 수신하면 각 요청 내용을 보고 적절한 주 샤드로 요청을 넘겨준다.
- 이 때 주 샤드로 넘어간 각 요청은 각자 독자적으로 수행하므로 요청간 순서를 100% 보장하지 않는다.
- 완전히 동일한 인덱스, _id, 라우팅 조합을 가진 요청은 반드시 동일한 주 샤드로 요청이 가므로 이때는 순서를 보장할수 있다.
bulk API의 성능
- 네트워크를 통해 요청을 여러 번 반복해서 호출해야 한다면 이를 묶어 한꺼번에 전송하는것이 일반적으로 성능상 이득이다.
- ES도 마찬가지로 단건 문서 API를 여러번 하는것보다 bulk API를 사용하는 편이 성능이 월등히 빠르다.
- bulk API 요청 1번에 몇개의 요청을 모아서 보내는 것이 성능상 적절한지는 정해져있지 않고, 각 요청의 크기나 데이터 특성 등을 보고 적절히 조절해야 한다.
- HTTP 요청을 chunked로 보내는 것은 성능을 떨어지게 만들기 떄문에 피해야 한다. ( HTTP Chunk 참고 )
4.2.2 multi get API
- multi get API는 _id를 여럿 지정하여 해당 문서를 한 번에 조회하는 API이다.
1
2
GET _mget
GET [인덱스 이름]/_mget
- _bulk API와 마찬가지로 _mget 앞에 인덱스 이름을 명시했다면 이 인덱스를 기본 인덱스로 지정한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
### _mget
POST /_mget
Host: localhost:9200
Content-Type: application/json
{
"docs": [
{
"_index": "bulk_test",
"_id": 1
},
{
"_index": "bulk_test",
"_id": 4,
"routing": "a"
},
{
"_index": "my_index2",
"_id": 1,
"_source": {
"include": ["p*"],
"exclude": ["point"]
}
}
]
}
- 요청했던 순서대로 문서의 내용을 모아 단일 응답으로 돌아온다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
"docs": [
{
"_index": "bulk_test",
"_id": "1",
"_version": 2,
"_seq_no": 3,
"_primary_term": 1,
"found": true,
"_source": {
"field1": "value1",
"field2": "value2"
}
},
{
"_index": "bulk_test",
"_id": "4",
"_version": 1,
"_seq_no": 4,
"_primary_term": 1,
"_routing": "a",
"found": true,
"_source": {
"field1": "value4"
}
},
{
"_index": "my_index2",
"_id": "1",
"_version": 1,
"_seq_no": 0,
"_primary_term": 1,
"found": true,
"_source": {}
}
]
}
4.2.3 update by query
update by query
는 앞서 명한 bulk API, multi get API와는 성격이 좀 다른 복수 문서 API이다.- 검색 쿼리를 통해 주어진 조건에 만족하는 문서를 찾은 뒤 그 문서를 대상으로 업데이트나 삭제 작업을 실시하는 API이다.
- 보통은 서비스에서 일상적으로 사용하기보다는 관리적인 목적으로 호출하는 것이 일반적이다.
1
2
3
4
5
6
7
8
9
10
POST [인덱스 이름]/_update_by_query
{
"script": {
"source": " // ... ",
},
"query": {
// ...
}
}
update by query 업데이트 지원 관련
- doc을 이용한 업데이트를 지원하지 않고, script를 통한 업데이트만 지원한다.
- painless 스크립트를 사용하는 경우 문맥 정보 중에서 ctx._now를 사용할 수 없다.(현재 타임스탬프값을 밀리세컨드로 반환한 값)
update by query 동작 방식
- ES는 query 절의 검색 조건에 맞는 문서를 찾아 일종의 스냅샷을 찍는다.
- 이후 각 문서마다 지정된 스크립트에 맞게 업데이트 실시
- 순차적으로 실행하는 과정중에 스냅샷을 찍어뒀던 문서에서 변화가 생긴 문서가 발견되면 이를 업데이트 하지 않는다.
- 버전 충돌 문제시 전체 작업을 그만두거나 다음 작업으로 넘어가는 방식을 사용자가 선택할 수 있다. ( conflicts 매개변수 지정, 기본값 abort)
- 도중에 충돌로 인해 중간에 작업이 중단된 경우 그 이전까지 업데이트된 내용이 롤백되거나 하지는 않는다(이를 염두해두고 작업을 수행해야 한다.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
### _update_by_query
POST /bulk_test/_update_by_query
Host: localhost:9200
Content-Type: application/json
{
"script": {
"source": "ctx._source.field1 = ctx._source.field1 + '_' + ctx._id",
"lang": "painless"
},
"query": {
"exists": {
"field": "field1"
}
}
}
- bulk_test 인덱스 내에 field1이라는 이름의 필드가 존재하는 문서를 대상으로 업데이트를 수행했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"took": 242,
"timed_out": false,
"total": 3,
"updated": 3,
"deleted": 0,
"batches": 1,
"version_conflicts": 0,
"noops": 0,
"retries": {
"bulk": 0,
"search": 0
},
"throttled_millis": 0,
"requests_per_second": -1.0,
"throttled_until_millis": 0,
"failures": []
}
- 응답을 보면 작업된 문서 개수, 버전 충돌, 재시도 횟수 등을 확인할 수 있다.
스로틀링
- 위 예시 응답 내용 중 throttled_millis, requests_per_second, throttled_until_millis 들을 볼 수 있는데 이는 스로틀링과 관련된 필드이다.
- update by query API는 문제가 생긴 데이터를 일괄적으로 처리하거나 변경된 비즈니스 요건에 맞게 데이터를 일괄 수정하는 작업 등에 많이사용되는데, 대량 작업을 수행하면 운영 중인 기존 서비스에도 영향을 줄수 있으므로 이런 상황을 피하기 위해 스로틀링 기능을 제공한다.
- 스로틀링 적용을 통해 작업의 속도를 조정하고 클러스터 부하와 서비스 영향을 최소화할 수 있다.
1
2
3
4
5
6
7
8
### throttling
POST /bulk_test/_update_by_query?scroll_size=1000&scroll=1m&requests_per_seconds=500
Host: localhost:9200
Content-Type: application/json
{
// ...
}
스로틀링 설정시 고려 사항
scroll_size
는 문서 검색/업데이트를 수행하는 단위 개수이다.- 1000개로 지정한다면 1000개의 문서를 가져온뒤 1000개 문서에 대한 업데이트를 수행하고, 그 이후에 다시 1000개를 가져오는 방식으로 동작한다.
- 서비스와 작업 환경에 맞는 적절한 scroll_size를 지정하는것이 필요하다.
scroll
은 검색 조건에 만족하는 모든 문서를 대상으로 검색이 처음 수행됐을 당시 상태를 검색 문맥(search context)에 저정하는데, 이 search context를 얼마나 보존할지에 대한 설정이다.- 1m으로 설정할 경우 1분 동안 검색 문맥이 유지된다.
- 모든 작업이 종료될 때까지 필요한 시간을 지정하는것은 아니고, 한 배치 작업에 필요한 시간을 지정하면 된다.(scroll_size만큼의 작업 처리)
- scroll_size만큼의 작업을 수행할 수 없을만큼 짧게 지정해서는 안된다.
- 너무 큰 값을 지정하면 힙이나 디스크 공간, file descriptor 등 많은 자원 소비가 필요하니 적절한 값을 지정해야 한다.
requests_per_secoond
은 이름 그대로 평균적으로 초당 몇 개까지의 작업을 수행할것인지에 대한 지정이다.- scroll_size 단위로 업데이트 작업을 수행한 뒤
requests_per_secoond
값에 맞도록 일정 시간을 대기하는 방식으로 진행되낟. - scroll_size 1000, requests_per_second를 500으로 설정했다면 ES는 2초마다 스크롤 한번 분량만큼을 업데이트한다.
- requests_per_secoond를
-1
로 설정하면 스로틀링을 적용하지 않는다.
- scroll_size 단위로 업데이트 작업을 수행한 뒤
비동기적 요청과 tasks API
- update 처리에 따라서 작업시간이 수십시간이 될수도 있는데, 이를 HTTP blocking 방식으로 응답을 무한정 대기할수는 없다.
- 이런 문제를 위해 update by query 요청시
wait_for_completion
매개변수를 false로 지정하여 비동기적 처리를 할 수 있다.- 이떄 응답으로 task id를 반환한다.
- task API는 비동기 처리시 반환받은 task_id를 가지고 현재 진행중인 task의 상태를 확인할 수 있다.
1
2
3
4
POST [인덱스 이름]/_update_by_query?wait_for_completion=false
{
// ...
}
1
2
3
{
"task" : "f8Aa9sj-RbaOebr3acDcxQ:120896"
}
tasks API
1
2
GET .tasks/_doc/[task id]
GET _tasks/[task id]
2개 API의 차이점
.tasks
API는 ES 내부 인덱스인.tasks
의 문서 정보를 단건으로 조회하는 방식이다.- 비동기로 등록되지 않은 작업도 확인할 수 있다.
_tasks
API는 tasks 관리 APAI인데 ES 8.11 기준으로도 beta 기능이라 변경될 가능성이 있다.
task API 호출
1
2
3
4
### task api
GET /_tasks/f8Aa9sj-RbaOebr3acDcxQ:120896
Host: localhost:9200
Content-Type: application/json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
{
"completed": true,
"task": {
"node": "f8Aa9sj-RbaOebr3acDcxQ",
"id": 120896,
"type": "transport",
"action": "indices:data/write/update/byquery",
"status": {
"total": 3,
"updated": 3,
"created": 0,
"deleted": 0,
"batches": 1,
"version_conflicts": 0,
"noops": 0,
"retries": {
"bulk": 0,
"search": 0
},
"throttled_millis": 0,
"requests_per_second": -1.0,
"throttled_until_millis": 0
},
"description": "update-by-query [bulk_test] updated with Script{type=inline, lang='painless', idOrCode='ctx._source.field1 = ctx._source.field1 + '_' + ctx._id', options={}, params={}}",
"start_time_in_millis": 1702726259365,
"running_time_in_nanos": 110659458,
"cancellable": true,
"cancelled": false,
"headers": {}
},
"response": {
"took": 104,
"timed_out": false,
"total": 3,
"updated": 3,
"created": 0,
"deleted": 0,
"batches": 1,
"version_conflicts": 0,
"noops": 0,
"retries": {
"bulk": 0,
"search": 0
},
"throttled": "0s",
"throttled_millis": 0,
"requests_per_second": -1.0,
"throttled_until": "0s",
"throttled_until_millis": 0,
"failures": []
}
}
task 작업 취소
- 작업 진행 중 문제가 발생했다면 다음과 같이 작업을 취소할 수 있다.
- 어떤 노드에서 작업이 취소되었는지, 어떤 task 작업이 취소됐는지 등을 확인할 수 있다.
wait_for_completion=true
로 지정해 호출한 작업이더라도 task id를 알아내서 작업취소할 수 있다.
1
POST _tasks/[task id]/_cancel
스로틀링 동적 변경
- 대량 작업을 수행하다 외부적인 이슈로 클러스터 전체에 문제가 발생하는 경우 스로틀링 동적 변경을 통해 대응할 수 있다.
- ES update_by_query 특성상 이미 처리 완료된 데이터에 대한 트랜잭션 롤백이 없으므로 중간에 task를 중단시키는것이 능사가 아닐수 있다.
- 72시간동안 수행되어야할 작업 중 일부만 남겨두고 있었다면 중간에 task를 취소하는것도 리스크일수 있음.
1
POST _update_by_query/[task id]/_rethrottle?request_per_second=[변경할 값]
- 위와 같이
_rethrottle
을 이용하면 작업의 스로틀링을 동적으로 변경할 수 있어 문제 상황을 유연하게 대응할 수 있다.
task 결과 삭제
wait_for_completion=false
을 통해.tasks
인덱스에 등록된 작업이 성공하거나 취소됐는지의 여부와 상관없이 등록된 작업 결과는 ES에 계속 남는다.- 작업의 상황을 충분히 확인했다면 인덱스의 문서를 삭제하면 좋다
1
DELETE .tasks/_doc/[task id]
슬라이싱
- 관리적 목적의 대량 업데이트를 수행하는 경우, 스로틀링을 적용해 부하를 줄이는 선택도 있겠지만, 반대로 업데이트 성능을 최대로 끌어내 빠른 시간 안에 끝내고자 하는 선택도 있다.(ex. 서비스 요청 차단 후 정기점검)
slices
매개변수를 지정하면 검색과 업데이트를 지정한 개수로 쪼개 병렬적으로 수행한다.- 기본값은 1이고, auto로 지정하는 경우 ES가 적절한 개수를 지정해서 작업을 병렬 수행한다.
1
2
3
4
POST [인덱스 이름]/_update_by_query?slice=auto
{
// ...
}
슬라이싱 관련 주의사항
slices=auto
인 경우 보통은 지정한 인덱스의 주 샤드 수가 슬라이스의 수로 지정된다.- 주 샤드 수보다 높은 slices 수를 지정하는 경우 오히려 성능이 급감할수 있음을 유의해야 한다.
- 샤드 내 slices 분배가 필요해지는데 슬라이스 수가 주 샤드 이내인 경우에는 필요 없었던 과정임.
- 슬라이싱은 기본적으로 샤드를 기준으로 작업을 쪼개는 것이기 때문에 각 요청 슬라이스가 동일한 작업량을 분배받는것은 아니다
- requests_per_second 옵션은 각 슬라이스에 쪼개져서 적용됨을 알아야 한다.
- requests_per_second=1000 + slices=5라면, 각 슬라이스는 200씩을 분배 받게 된다.
4.2.4 delete by query
- delete by query는 update by query처럼 먼저 지정한 검색 쿼리로 삭제할 대상을 지정한 뒤에 삭제를 수행하는 작업이다.
- 보통은 주기적인 배치성 작업으로 오래된 데이터 등 더이상 사용하지 않는 데이터를 삭제하는 경우에 사용된다.
1
2
3
4
5
6
7
POST [인덱스 이름]/_delete_by_query
{
"query": {
// ...
}
}
delete by query 동작 방식
- update by query와 마찬가지로 검색 조건에 맞는 문서를 찾아 스냅샷을 찍는다.
- 삭제 작업이 진행되는 동안 문서의 내용이 변경됐다면 버전 충돌이 일어날떄의 처리 등 update와 동일하다.
- 이 외에 tasks API를 통한 관리, 스로틀링 적용, 슬라이싱 적용 등 모두 update와 동일하다.
4.3 검색 API
- ES의 기본이자 핵심은 검색엔진이다.
- 자주 사용되는 검색 쿼리, 쿼리 문맥과 필터 문맥, 검색 결과 정렬, 페이지네이션을 학습해보자.
4.3.1 검색 대상 지정
- ES는 다양한 종류의 검색 쿼리를 제공한다.
- GET/POST 중 무엇을 사용하더라도 동작은 동일하다.
1
2
3
4
GET [인덱스 이름]/_search
POST [인덱스 이름]/_search
GET _search
POST _search
검색 관련
- 인덱스 이름을 지정하지 않으면 전체 인덱스에 대해 검색한다.
- 인덱스 범위를 최대한 좁혀서 검색해야 성능 부담이 적으므로 일반적으로는 인덱스 이름을 명시적으로 지정한다.
- 인덱스 이름을 지정할 떄는 와일드카드 문자(*)와 콤마로 구분하여 검색 대상을 여럿 지정하는것도 가능하다.
1
2
3
4
### _search
GET /my_index*,analyzer_test*,mapping_test/_search
Host: localhost:9200
Content-Type: application/json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{
"took": 76,
"timed_out": false,
"_shards": {
"total": 3,
"successful": 3,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 3,
"relation": "eq"
},
"max_score": 1.0,
"hits": [
{
"_index": "my_index2",
"_id": "3",
"_score": 1.0,
"_source": {
"title": "hello world",
"views": 1234,
"public": true,
"point": 4.5,
"created_at": "2019-01-17T14:05:01.234Z"
}
},
{
"_index": "my_index2",
"_id": "1",
"_score": 1.0,
"_source": {
"hello": "world2"
}
},
{
"_index": "my_index2",
"_id": "2",
"_score": 1.0,
"_source": {
"hello": "world2"
}
}
]
}
}
- 위 예시처럼 검색 대상만 지정하고 쿼리 종류와 질의어를 지정하지 않으면 지정한 대상 내 모든 문서가 hit된다.
4.3.2 쿼리 DSL 검색과 쿼리 문자열 검색
- ES에서는 크게 요청 본문에 쿼리 DSL을 기술하여 검색하는 방법과 요청 주소줄에 q라는 매개변수를 넣고 그곳에 루씬 쿼리 문자열을 지정하는 2가지 방법을 제공한다.
쿼리 DSL 검색
- 아래와 같이 요청 본문에 query 필드를 넣어 그 안에 원하는 쿼리와 질의어를 기술한다.
1
2
3
4
5
6
7
8
9
10
11
12
### 쿼리 DSL
GET /my_index2/_search
Host: localhost:9200
Content-Type: application/json
{
"query": {
"match": {
"title" : "hello"
}
}
}
쿼리 문자열 검색
- q 매개변수에 루씬 쿼리 문자열을 넣어서 하는 방식은 다음과 같다.
- 이 요청 방식 특성상 복잡한 쿼리르 지정하기 어렵고 긴 쿼리르 전달하는 것도 부담스럽기 때문에 보통은 간단한 요청을 이용하는 경우 사용된다.
1
2
3
4
### 쿼리 문자열
GET /my_index2/_search?q=title:hello
Host: localhost:9200
Content-Type: application/json
루씬 쿼리 문자열 문법 몇가지
루씬 쿼리 문자열 관련 참고사항
- 와일드카드 검색은 서비스환경에서는 사실상 사용하지 않는것이 좋다.
*ello
나?ello
처럼 와일드카드 문자가 앞에 오는 쿼리는 부담이 더욱 심하다.(인덱스가 들고있는 모든 term을 가지고 검색을 해야함)- 단 한번의 쿼리로 ES 클러스터 전체를 다운시킬 수도 있다.
indices.query.query_string.allowLeadingWildcard
설정을 false로 지정하면 와일드카 문자가 앞에 오는 쿼리르 막을 수 있다.search.allow_expensive_queries
설정을 false로 지정하면 와일드카드 검색을 포함한 몇몇 무거원 쿼리를 사용한 검색 자체를 아예 막을 수 있다.
4.3.3 match_all 쿼리
- match_all 쿼리는 모든 문서를 매치하는 쿼리이다.(query 부분을 비워두면 기본값으로 지정됨)
1
2
3
4
5
6
GET [인덱스 이름]/_search
{
"query": {
"math_all": {}
}
}
4.3.4 match 쿼리
- match는 지정한 필드의 내용이 질의어와 매치되는 문서를 찾는 쿼리이다.
- 필드가 text 타입이라면 질의어도 모두 애널라이저로 분석된다.
1
2
3
4
5
6
7
8
9
10
11
12
GET [인덱스 이름]/_search
{
"query": {
"match": {
"fieldName": {
"query": "test query sentence",
"operator": "and"
}
}
}
}
- 위 예제를 기준으로 설명하면 다음과 같다.
- 인덱스의 fieldName 필드가 text타입고, standard 애널라이저를 사용한다면, 검색 쿼리도 test, query, sentence 총 3개의 토큰으로 분석된다.
- match 쿼리는 기본동작이 OR로 동작하는데 위 예시에서는
"operator": "and"
을 추가하여 위 3개의 텀에 모두 매칭되는 경우에만 검색이 이루어진다.
4.3.5 term 쿼리
- term 쿼리는 지정한 필드의 값이 질의어와 정확히 일치하는 문서를 찾는 쿼리이다.
- 필드 타입에 지정된것과 동일하게 처리됨.
- 대상 필드에 노멀라이저가 지정되어 있다면 질의어도 노멀라이저 처리를 거친다.
1
2
3
4
5
6
7
8
9
10
11
GET [인덱스 이름]/_search
{
"query": {
"term": {
"fieldName": {
"value": "hello"
}
}
}
}
term 쿼리 주의사항
- keyword 타입과 보통 잘 어울리는 쿼리이다.
- text 타입의 필드를 대상으로 하는 경우 질의어는 노멀라이저 처리를 거치지만, 필드의 값은 애널라이저로 분석한 뒤 생성된 역색인을 이용하게 된다.
4.3.6 terms 쿼리
- term 쿼리와 매우 유사한데, 질의어를 여러개 지정할 수 있고, 하나 이상의 질의어가 일치하면 검색 결과에 포함하는 쿼리이다.
1
2
3
4
5
6
7
8
9
GET [인덱스 이름]/_search
{
"query": {
"terms": {
"fieldName": ["hello", "world"]
}
}
}
4.3.7 range 쿼리
- range 쿼리는 지정한 필드의 값이 특정 범위 내에 있는 문서를 찾는 쿼리이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
### ex1
GET [인덱스 이름]/_search
{
"query": {
"range": {
"fieldName": {
"gte": 100,
"lt": 200
}
}
}
}
### ex2 - date
GET [인덱스 이름]/_search
{
"query": {
"range": {
"fieldName": {
"gte": "2019-01-15T00:00:00.000Z||+36h/d",
"lt": "now-3h/d"
}
}
}
}
- gt(greater than), lt(less than), gte, lte를 지정하여 range 검색을 할 수 있다.
range query 주의사항
- ES 문자열 필드 대상으로 한 range 쿼리를 부학가 큰 쿼리로 분류한다.
- 사용시 데이터 양상을 파악하고 부담이 없는 상황에서만 사용해야 한다.
search.allow_expensive_queries
설정을 false로 지정하면 막을 수 있다.
range query에 date를 사용하는 경우 표현식
now
: 현재시각을 나타낼 수 있다. -||
: 날짜 시간 문자열의 마지막에 붙이고, 이 뒤에 붙는 문자열은 시간 계산식으로 파싱된다.+, -
: 지정된 시간만큼 더하거나 빼는 연산을 수행한다./
: 버림을 수행한다. (ex. /d는 날짜 단위 이하의 시간을 버림한다.)
4.3.8 prefix 쿼리
- prefix 쿼리는 필드의 값이 지정한 질의어로 시작하는 문서를 찾는 쿼리이다.
1
2
3
4
5
6
7
8
9
10
11
GET [인덱스 이름]/_search
{
"query": {
"prefix": {
"fieldName": {
"value": "hello"
}
}
}
}
prefix 사용시 주의사항
- preifx도 무거운 쿼리로 분류되나, 와일드카드 검색처럼 아예 사용하지 말아야 할 정도는 아니다.
- prefix를 서비스 호출 용도로 사용하려 한다면 인덱스 필드 맵핑 설정에
index_prefixes
설정을 넣는 방법이 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
### index_prefixes
PUT /prefix_mapping_test
Host: localhost:9200
Content-Type: application/json
{
"mappings": {
"properties": {
"prefixField": {
"type": "text",
"index_prefixes": {
"min_chars": 3,
"max_chars" : 5
}
}
}
}
}
- 위처럼 index_prefixes를 지정하면 ES 문서를 색인할 때 min_chars와 max_chars 사이의 prefix를 미리 별도 색인한다.
- 색인 크기와 색인 속도에서 손해를 보는 대신 prefix 쿼리의 성능을 높일 수 있다.
- min_chars의 기본값은 2이고, max_chars의 기본값은 5이다.
search.allow_expensive_queries
설정을 false로 지정하면, index_prefixes가 적용되지 않은 prefix 쿼리는 사용할 수 없다.
4.3.9 exists 쿼리
- exists 쿼리는 지정한 필드를 포함한 문서를 검색한다.
1
2
3
4
5
6
7
8
9
GET [인덱스 이름]/_search
{
"query": {
"exists": {
"field": "fieldName"
}
}
}
4.3.10 bool 쿼리
- bool 쿼리는 여러 쿼리를 조합하여 검색하는 쿼리이다.
- must, must_not, filter, shoulde의 4가지 종류의 조건절에 다른 쿼리를 조합하여 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
}
}
}
- must 조건절과 filter 조건절에 들어간 하위 쿼리는 모두 AND 조건으로 만족해야 최종 검색 결과에 포함된다.
- must_not 조건절에 들어간 쿼리를 만족하는 문선느 최종 검색 결과에서 제외된다.
- should 조건절에 들어간 쿼리는 minimum_should_match에 지정한 개수 이상의 하위 쿼리를 만족하는 문서가 최종 검색 결과에 포함된다.
- minimum_should_match의 기본값은 1이고, 이 경우 should 조건절에 들어간 쿼리는 OR조건으로 검색하는 것으로 이해할 수 있다.
쿼리 문맥과 필터 문맥
- must와 filter는 모두 AND 조건으로 검색을 수행하지만, 점수를 계산하느냐 여부가 다르다.
- filter 조건에 들어간 쿼리는 단순히 문서 매치 여부만을 판단하고, 랭킹에 사용할 점수를 매기지 않는다.
- must_not도 점수를 매기지 않는다.
- 점수를 매기지 않고 단순히 조건을 만족하는지 여부를 참 또는 거짓으로 따지는 검색과정을
필터 문맥(filter context)
라고 한다. - 조건을 얼마나 더 만족하는지 유사도 점수를 매기는 검색 과정을
쿼리 문맥(query context)
라고 한다. - 유사도가 필요하지 않은 경우 필터 문맥으로 검색해야 성능상 유리하다.
- 쿼리 문맥
- 상대적으로 성능이 느리고, 쿼리 캐시 활용 불가
- bool.must, bool.should, match, term
- 필터 문맥
- 상대적으로 성능이 빠르고, 쿼리 캐시 활용 가능
- bool.filter, bool.must_not, exists, range, constant_score
쿼리 수행 순서
- bool 쿼리를 사용하여 여러 쿼리를 조합하는 경우 어떤 실행순러ㅗ 수행된다는 규칙 이없다.
- ES 내부적으로 쿼리를 루씬의 여러 쿼리로 쪼갠 뒤 조합하여 재작성하는데, 이 각 쿼리별 비용이 얼마나 소요될지는 내부적으로 추정하고, 이 추정된 비용을 기준으로 무엇을 먼저 실행할지 내부적으로 결정된다.
- 또 하부 쿼리를 병렬 수행하기도 하고, 복잡한 과정을 거친다.
- 만약 내부적인 쿼리 실행 순서를 원하는대로 해야한다면 커스텀 플러그인에서 커스텀 쿼리를 만들어야 한다.
4.3.11 constant_score 쿼리
- constant_score 쿼리는 하위 filter 부분에 지정한 쿼리를 필터 문맥에서 검색하는 쿼리이다.
- 유사도는 일괄적으로 1로 지정된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
### 4.3.11 constant_score 쿼리
GET [인덱스 이름]/_search
{
"query": {
"constant_score": {
"filter" : {
"term": {
"fieldName": "hello"
}
}
}
}
}
4.3.12 그 외 주요 매개변수
- 쿼리 종류와 관계 없이 검색 API에 공통적으로 적용할 수 있는 주요 매개변수 몇가지를 알아보자
라우팅
- 라우팅 지정 여부가 가져오는 성능차이가 크다.
- 라우팅을 지정할 수 있는 경우라면 최대한 라우팅 이득을 볼수 있도록 설계하자(ex. 특정 사용자 기준으로 데이터를 관리하는 경우)
1
2
3
4
5
6
7
8
### 라우팅
GET [인덱스이름]/_search?routing=i[라우팅]
{
"query": {
// ...
}
}
explain
- 검색을 수행하는 동안 쿼리의 각 하위 부분에서 점수가 어떻게 계산됐는지 설명을 확인하기 위해 explain을 사용할 수 있다.
1
2
3
4
5
6
7
GET [인덱스이름]/_search?explain=true
{
"query": {
// ...
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
GET /my_index3/_search?explain=true
Host: localhost:9200
Content-Type: application/json
{
"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
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
{
"took": 23,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 1.1507283,
"hits": [
{
"_shard": "[my_index3][0]",
"_node": "b_fDpGOqSWOciib82ngCTA",
"_index": "my_index3",
"_id": "1",
"_score": 1.1507283,
"_source": {
"field1": "hello",
"field2": "world",
"field3": true,
"field4": "elasticsearch",
"field5": "lucene"
},
"_explanation": {
"value": 1.1507283,
"description": "sum of:",
"details": [
{
"value": 0.2876821,
"description": "weight(field1:hello in 0) [PerFieldSimilarity], result of:",
"details": [
{
"value": 0.2876821,
"description": "score(freq=1.0), computed as boost * idf * tf from:",
"details": [
{
"value": 2.2,
"description": "boost",
"details": []
},
{
"value": 0.2876821,
"description": "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:",
"details": [
{
"value": 1,
"description": "n, number of documents containing term",
"details": []
},
{
"value": 1,
"description": "N, total number of documents with field",
"details": []
}
]
},
{
"value": 0.45454544,
"description": "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:",
"details": [
{
"value": 1.0,
"description": "freq, occurrences of term within document",
"details": []
},
{
"value": 1.2,
"description": "k1, term saturation parameter",
"details": []
},
{
"value": 0.75,
"description": "b, length normalization parameter",
"details": []
},
{
"value": 1.0,
"description": "dl, length of field",
"details": []
},
{
"value": 1.0,
"description": "avgdl, average length of field",
"details": []
}
]
}
]
}
]
},
{
"value": 0.2876821,
"description": "weight(field2:world in 0) [PerFieldSimilarity], result of:",
"details": [
{
"value": 0.2876821,
"description": "score(freq=1.0), computed as boost * idf * tf from:",
"details": [
{
"value": 2.2,
"description": "boost",
"details": []
},
{
"value": 0.2876821,
"description": "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:",
"details": [
{
"value": 1,
"description": "n, number of documents containing term",
"details": []
},
{
"value": 1,
"description": "N, total number of documents with field",
"details": []
}
]
},
{
"value": 0.45454544,
"description": "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:",
"details": [
{
"value": 1.0,
"description": "freq, occurrences of term within document",
"details": []
},
{
"value": 1.2,
"description": "k1, term saturation parameter",
"details": []
},
{
"value": 0.75,
"description": "b, length normalization parameter",
"details": []
},
{
"value": 1.0,
"description": "dl, length of field",
"details": []
},
{
"value": 1.0,
"description": "avgdl, average length of field",
"details": []
}
]
}
]
}
]
},
{
"value": 0.2876821,
"description": "weight(field4:elasticsearch in 0) [PerFieldSimilarity], result of:",
"details": [
{
"value": 0.2876821,
"description": "score(freq=1.0), computed as boost * idf * tf from:",
"details": [
{
"value": 2.2,
"description": "boost",
"details": []
},
{
"value": 0.2876821,
"description": "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:",
"details": [
{
"value": 1,
"description": "n, number of documents containing term",
"details": []
},
{
"value": 1,
"description": "N, total number of documents with field",
"details": []
}
]
},
{
"value": 0.45454544,
"description": "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:",
"details": [
{
"value": 1.0,
"description": "freq, occurrences of term within document",
"details": []
},
{
"value": 1.2,
"description": "k1, term saturation parameter",
"details": []
},
{
"value": 0.75,
"description": "b, length normalization parameter",
"details": []
},
{
"value": 1.0,
"description": "dl, length of field",
"details": []
},
{
"value": 1.0,
"description": "avgdl, average length of field",
"details": []
}
]
}
]
}
]
},
{
"value": 0.2876821,
"description": "weight(field5:lucene in 0) [PerFieldSimilarity], result of:",
"details": [
{
"value": 0.2876821,
"description": "score(freq=1.0), computed as boost * idf * tf from:",
"details": [
{
"value": 2.2,
"description": "boost",
"details": []
},
{
"value": 0.2876821,
"description": "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:",
"details": [
{
"value": 1,
"description": "n, number of documents containing term",
"details": []
},
{
"value": 1,
"description": "N, total number of documents with field",
"details": []
}
]
},
{
"value": 0.45454544,
"description": "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:",
"details": [
{
"value": 1.0,
"description": "freq, occurrences of term within document",
"details": []
},
{
"value": 1.2,
"description": "k1, term saturation parameter",
"details": []
},
{
"value": 0.75,
"description": "b, length normalization parameter",
"details": []
},
{
"value": 1.0,
"description": "dl, length of field",
"details": []
},
{
"value": 1.0,
"description": "avgdl, average length of field",
"details": []
}
]
}
]
}
]
},
{
"value": 0.0,
"description": "match on required clause, product of:",
"details": [
{
"value": 0.0,
"description": "# clause",
"details": []
},
{
"value": 1.0,
"description": "field3:T",
"details": []
}
]
}
]
}
}
]
}
}
_explanation
부분에서 점수 계산 과정의 상세한 설명을 확인 할 수 있다._explanation
을 만들기 위해 내부적으로 쿼리를 덜 최적화해 수행하기도 하므로, 성능이 비교적 하락할 수 있다.- 서비스 환경에서는 사용하지 않고 디버깅 용도로만 사용하는 것이 좋다.
- 각 조건들을 검색할떄 나왔던 score를 합산하여 최종 score가 결정됨.
search_type
- search_type을 지정하면 유사도 점수를 계산할 때 각 샤드 레벨에서 계산을 끝낼지 여부를 선택할 수 있다.
1
2
3
4
5
6
7
8
GET [인덱스 이름]/_search?search_type=dfs_query_then_fetch
{
"query": {
// ...
}
}
- query_then_fetch(기본값)
- 각 샤드 레벨에서 유사도 점수 계싼을 끝낸다.
- 점수 계산이 약간 부정확할 수 있지만, 검색 성능의 차이가 크기 때문에 특별한 경우가 아니라면 이대로 사용하는것을 권장
- dfs_query_then_fetch
- 모든 샤드로부터 정보를 모아 유사도 점수를 글로벌하게 계산한다.
- 점수의 정확도는 올라가지만 검색 성능은 떨어진다.
4.3.13. 검색 결과 정렬
- 요청 본문에
sort
를 지정하면 검색 결과를 정렬할 수 있다. - 정렬 대상 필드를 여러개로 지정하는 경우 요청에 지정한 순서대로 정렬을 수행한다.
1
2
3
4
5
6
7
8
9
10
11
12
GET [인덱스 이름]/_search
{
"query": {
// ...
},
"sort": [
{"field1": {"order": "desc"} },
{"field2": {"order": "asc"} },
"field3"
]
}
정렬시 유의사항
- ES에서는 필드타입에 따라서 정렬에 사용할 수 있는 타입과 불가능한 타입이 있다.
- 숫자, date, boolean, keyword 타입은 정렬 대상이 될수 있다.
- text 타입은 정렬 불가능하다.
- fielddata를 true로 지정하면 text 타입도 정렬에 사용할수 있으나, 성능문제가 있으므로 사용을 지양해야 한다.
- 정렬에서 _score, _doc을 지정해서 정렬을 수행할 수도 있다.
- sort 기본값은 _score 내림차순으로 조회된다.
- 정렬 수행을 위해서는 필드의 값이 메모리에 올라가야하는데 운영환경 서비스 사용시 정렬 대상 필드를 integer, short, float 등 타입으로 설계하는 것도 좋은 방법이다.
4.3.14. 페이지네이션
- 검색 결과를 페이지네이션하는 방법을 알아보자.
from과 size
- size는 검색 API의 결과로 몇 개의 문서를 반환할것인지를 지정한다.(기본값 : 10)
- from은 몇 번쨰 문서부터 결과를 반환할지에 대한 오프셋을 지정한다.(기본값 : 0)
1
2
3
4
5
6
7
8
9
GET [인덱스 이름]/_search
{
"from": 0,
"size": 10,
"query": {
// ...
}
}
from과 size 방식의 한계점
- 실제로 페이지네이션을 구현할때 from, size는 제한적으로 사용해야 한다.
- 내부적으로 MySQL의 limit-offset 방식의 쿼리처럼 동작하여 페이지 후반부로 갈수록 성능 이슈가 있을수 있음.
- 예를 들어 from:15, size:5로 지정한 검색을 하는 경우 상위 20개 문서를 가져온 뒤에 하위 5개만 잘라서 반환하는 방식으로 동작한다.
- 이전 페이지를 검색할 떄의 상태와 페이지를 넘기고 다음 검색을 수행할 때 인덱스 상태가 동일하지 않을수 있다는 문제가 있다.
- 페이징 검색 요청 사이에 새로운 문서가 색인되거나 삭제될 경우 의도한 페이지의 신규 데이터를 조회하는것이 아니라 데이터가 겹칠수 있다.
- 엄격한 페이지네이션을 제공해야 할 경우 from, size는 사용하지 않아야 한다.
- ES에서 from + size 합이 10,000을 넘어서는 검색은 수행을 제한하고 있다.
index.max_result_window
값을 조정해 변경할 수 있지만 비권장
scroll
- scroll은 검색 조건에 매칭되는 전체 문서를 모두 순회해야 할 때 적합한 방법이다.
- scroll은 순회하는 동안에는 최초 검색시 문맥(search_context)가 유지되어, 중복이나 누락이 발생하지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
GET [인덱스 이름]/_search?scroll=1m
{
"size": 1000,
"query": {
"bool": {
"must" : [
{ "term" : {"field1": {"value": "hello"} } },
{ "term" : {"field2": {"value": "world"} } }
]
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"_scroll_id": "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFnA3S3pUN1JnU1dpYzdVU2tPSEltVGcAAAAAAAACDhZiX2ZEcEdPcVNXT2NpaWI4Mm5nQ1RB",
"took": 5,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 0.5753642,
"hits": [
{
"_index": "my_index3",
"_id": "1",
"_score": 0.5753642,
"_source": {
"field1": "hello",
"field2": "world",
"field3": true,
"field4": "elasticsearch",
"field5": "lucene"
}
}
]
}
}
- 첫번쨰 scroll 검색을 할떄 scroll_id가 반환되는데, 이 값을 기준으로 scroll 검색을 수행하면 되고, 빈 hits 응답이 올때까지 scroll 검색을 반복하면 된다.
1
2
3
4
5
6
7
8
GET /_search/scroll
Host: localhost:9200
Content-Type: application/json
{
"scroll_id": "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFnA3S3pUN1JnU1dpYzdVU2tPSEltVGcAAAAAAAACSBZiX2ZEcEdPcVNXT2NpaWI4Mm5nQ1RB",
"scroll": "1m"
}
수동 검색 문맥 삭제
- 빠른 자원 반납을 위해 아래와 같이 명시적으로 검색 문맥을 삭제할수 있다.
1
2
3
4
5
6
7
DELETE /_search/scroll
Host: localhost:9200
Content-Type: application/json
{
"scroll_id": "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFnA3S3pUN1JnU1dpYzdVU2tPSEltVGcAAAAAAAACSBZiX2ZEcEdPcVNXT2NpaWI4Mm5nQ1RB"
}
scroll 검색시 성능 향상
- scroll 검색은 검색 결과의 정렬 여부가 상관없는 작업에 주로 사용하는 경우가 많은데, 이 경우
_doc
으로 정렬을 지정하면 유사도 점수를 계산하지 않으며, 정렬을 위한 별도의 자원도 사용하지 않아서 성능을 끌어올릴 수 있다.- sort의 기본값이 _score desc임을 기억하자
1
2
3
4
5
6
7
8
9
GET [인덱스 이름]/_search?scroll=1m
{
"size": 1000,
"query": {
// ...
},
"sort": ["_doc"]
}
scroll의 사용 목적
- scroll API는 사실 서비스에서 지속적으로 호출하는 용도로는 적합하지 않다.
- 주로 대량의 데이터를 다른 스토리지로 이전하거나 덤프하는 용도로 사용된다.
search_after
- 서비스에서 사용자에게 검색 결과를 페이지네이션으로 제공하는 경우
search_after
을 사용하는 것이 가장 적합하다. - search_after는 기본적으로 search의 sort을 활용하여 처리한다.
- 이 떄 정렬조건이 동점이 나오지 않도록 정렬 필드를 지정해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
GET /kibana_sample_data_ecommerce/_search
Host: localhost:9200
Content-Type: application/json
{
"size": 20,
"query": {
"term": {
"currency" : {
"value": "EUR"
}
}
},
"sort": [
{
"order_date": "desc"
},
{
"order_id": "asc"
}
]
}
1
2
3
4
"sort": [
1704573590000,
"591924"
]
- 첫 번쨰 검색이 끝나면 검색 결과 sort 값을 가져와 search_after 부분에 넣어 그 다음 검색을 요청할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
### search_after next
GET /kibana_sample_data_ecommerce/_search
Host: localhost:9200
Content-Type: application/json
{
"size": 20,
"search_after" : [ 1704573590000, "591924" ],
"query": {
"term": {
"currency" : {
"value": "EUR"
}
}
},
"sort": [
{
"order_date": "desc"
},
{
"order_id": "asc"
}
]
}
search_after의 동점 제거용 필드 지정시 주의사항
_id
값을 동점 제거용 기준 필드로는 사용을 지양해야 한다._id
필드는 doc_values가 꺼져 있기 때문에 이를 기준으로 정렬하게 되면 많은 메모리를 사용하게 된다.
- 동점 제거용 필드를 제대로 지정했다 하더라도, 인덱스 상태가 변하는 도중이라면 페이지네이션 과정에서 누락되는 문서가 발생할수 있다.
- 인덱스의 상태를 특정 시점으로 고려하려면 point in time API를 함께 조합해서 사용해야 한다.
point in time API
- point in time API는 검색 대상의 상태를 고정할 때 사용한다. keep_live 매개변수에 상태를 유지할 시간을 지정한다.
1
2
3
POST /kibana_sample_data_ecommerce/_pit?keep_alive=1m
Host: localhost:9200
Content-Type: application/json
1
2
3
{
"id": "gcSHBAEca2liYW5hX3NhbXBsZV9kYXRhX2Vjb21tZXJjZRZsMFQydEtZeVRuZWdHQmhxRjh0STV3ABZmOEFhOXNqLVJiYU9lYnIzYWNEY3hRAAAAAAAAAAueFkdhdW1CUXBtUmFLdURtam1SdFhiVVEAARZsMFQydEtZeVRuZWdHQmhxRjh0STV3AAA="
}
- 이렇게 얻은 pit id를 search_after와 같은곳에서 활용할 수 있다.
- pit id가 있는 상태에서는 대상 인덱스를 지정하지 않고 search한다.
- pit를 지정하는 것 자체가 검색 대상을 지정하는것이기 떄문이다.
- pit을 지정하면 동점 제거용 필드도 별도로 지정할 필요가 없다.
- search_after를 사용하는것은 이 search api 응답에 전달받은 sort 값을 기준으로 동일하게 사용하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
GET /_search
Host: localhost:9200
Content-Type: application/json
{
"size": 20,
"query": {
"term": {
"currency" : {
"value": "EUR"
}
}
},
"sort": [
{
"order_date": "desc"
},
{
"order_id": "asc"
}
],
"pit": {
"id": "gcSHBAEca2liYW5hX3NhbXBsZV9kYXRhX2Vjb21tZXJjZRZsMFQydEtZeVRuZWdHQmhxRjh0STV3ABZmOEFhOXNqLVJiYU9lYnIzYWNEY3hRAAAAAAAAAAw1FkdhdW1CUXBtUmFLdURtam1SdFhiVVEAARZsMFQydEtZeVRuZWdHQmhxRjh0STV3AAA=",
"keep_alive": "1m"
}
}
pit 명시적인 삭제
- pit을 다 사용한 뒤에는 명시적으로 삭제 할 수 있다.
1
2
3
4
5
6
7
DELETE /_pit
Host: localhost:9200
Content-Type: application/json
{
"id": "gcSHBAEca2liYW5hX3NhbXBsZV9kYXRhX2Vjb21tZXJjZRZsMFQydEtZeVRuZWdHQmhxRjh0STV3ABZmOEFhOXNqLVJiYU9lYnIzYWNEY3hRAAAAAAAAAAw1FkdhdW1CUXBtUmFLdURtam1SdFhiVVEAARZsMFQydEtZeVRuZWdHQmhxRjh0STV3AAA="
}
Comments powered by Disqus.