3. 인덱스 설계 - 1
- ES의 인덱스는 아주 세부적인 부분까지 설정으로 제어할 수 있다.
- 이 설정에 따라 동작과 특성이 매우 달라지므로 인덱스 설계에 신경써야 한다.
- 맵핑, 필드 타입, 애널라이저 등 핵심적인 설정을 학습해 ES 인덱스에 대한 이해도를 높여보자.
3.1 인덱스 설정
- 인덱스 생성시 동작에 관한 설정을 지정할 수 있다.
인덱스 설정 조회
1
[GET] [인덱스 이름]/_settings
1
2
3
GET /my_index/_settings
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
{
"my_index": {
"settings": {
"index": {
"routing": {
"allocation": {
"include": {
"_tier_preference": "data_content"
}
}
},
"number_of_shards": "1",
"provided_name": "my_index",
"creation_date": "1700298266901",
"number_of_replicas": "1",
"uuid": "O1_nsfp1Rc-f8RPSSUiWUg",
"version": {
"created": "8050299"
}
}
}
}
}
인덱스 설정 변경
1
2
3
4
5
[PUT] [인덱스 이름]/_settings
{
[변경할 내용]
}
1
2
3
4
5
6
7
PUT /my_index/_settings
Host: localhost:9200
Content-Type: application/json
{
"index.number_of_replicas": 0
}
1
2
3
{
"acknowledged": true
}
3.1.1 number_of_shards
- number_of_shards는 이 인덱스가 데이터를 몇 개의 샤드로 쪼갤 것인지를 지정하는 값이다.
- 이 값은 매우 신중하게 설계해야 한다.
- 한번 지정하면 reindex 같은 동작을 통해 인덱스를 통째로 재색인하는 등 특별한 작업을 수행하지 않는 한 바꿀수 없기 떄문이다.
- 샤드 개수를 어떻게 지정하느냐는 ES 클러스터 전체의 성능에도 큰 영향을 미친다.
- ES7부터 기본값은 1(이전에는 5였음)
주의 사항
- 샤드 하나마다 루씬 인덱스가 하나씩 더 생성된다는 사실과 주 샤드 하나당 복제본 샤드도 늘어난다는 사실을 염두에 둬야 한다.
- 샤드 숫자가 너무 많아지면 클러스터 성능이 떨어지고, 색인 성능이 감소한다.
- 인덱스당 샤드 숫자를 작게 지정하면 샤드 하나의 크기가 커진다. 샤드의 크기가 지나치게 커지면 장애 상황 등에서 샤드 복구에 너무 많은 시간이 소요되고, 클러스터 안정성이 떨어진다.
- 실제 운영을 하게 된다면 대량 데이터를 담기 위해 이 값을 적절한 값으로 꼭 조정해줘야 한다.
3.1.2 number_of_replicas
- 주 샤드 하나당 복제본 샤드를 몇개 둘 것인지를 지정하는 설정
- ES 클러스터에 몇 개의 노드를 붙일 것이며, 어느 정도 고가용성을 제공할 것인지 등을 고려해서 지정하면 된다.
- 0으로 설정할 경우 복제본 샤드를 생성하지 않고 주 샤드만 두는 설정이다.
- 복제본 샤드를 생성하지 않는 설정은 주로 대용량 초기 데이터를 마이그레이션 하는 등의 시나리오에서 쓰기 성능을 일시적으로 끌어올리기 위해 사용한다.
3.1.3 refresh_interval
- ES가 해당 인덱스를 대상으로 refresh를 얼마나 자주 수행할 것인지를 지정한다.
- 색인된 문서는 refresh되어야 하는 검색 대상이 되기 때문에 중요한 설정이다.
-1
로 값을 지정하면 주기적 refresh를 수행하지 않는다.- 설정 조회시 명시적으로 설정돼 있지 않은 경우 1초 defaut로 수행한다.(기본값 설정으로 되돌린다면 null로 업데이트하면된다)
1
2
3
4
5
6
7
PUT /my_index/_settings
Host: localhost:9200
Content-Type: application/json
{
"index.refresh_interval": "1s"
}
3.1.4 인덱스 설정을 지정하여 인덱스 생성
- 별도의 설정 없이 인덱스를 생성하는 경우 기본 설정으로 생성되는데, 설정을 포함하여 인덱스를 생성할 수 있다.
1
2
3
4
5
6
7
PUT [인덱스 이름]
{
"settings": {
[인덱스 설정]
}
}
}
1
2
3
4
5
6
7
8
9
10
PUT /my_index2
Host: localhost:9200
Content-Type: application/json
{
"settings": {
"index.number_of_shards": 2,
"index.number_of_replicas": 2
}
}
1
2
3
4
5
{
"acknowledged": true,
"shards_acknowledged": true,
"index": "my_index2"
}
acknowledged
는 해당 인덱스가 클러스터에 제대로 생성되었는지 여부를 나타낸다.shards_acknowledged
값은 타임아웃이 떨어지기 전에 지정한 개수만큼 샤드가 활성화되었는지를 나타낸다.wait_for_active_shareds
인자로 해당 개수를 지정할 수 있다.- 기본적으로는 1개의 샤드, 즉 주샤드가 시간 안에 활용화되면 true를 반환한다.
3.1.5 인덱스 삭제
1
DELETE [인덱스 이름]
1
2
DELETE /my_index2
Host: localhost:9200
3.1.6 인덱스 설정 재대로 적용되었는지 조회
1
2
GET /my_index2
Host: localhost:9200
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
{
"my_index2": {
"aliases": {},
"mappings": {},
"settings": {
"index": {
"routing": {
"allocation": {
"include": {
"_tier_preference": "data_content"
}
}
},
"number_of_shards": "2",
"provided_name": "my_index2",
"creation_date": "1700304795832",
"number_of_replicas": "2",
"uuid": "l2KPoucXQbOwxk1W0oto4A",
"version": {
"created": "8050299"
}
}
}
}
}
3.2 매핑과 필드 타입
- 매핑은 문서가 인덱스에 어떻게 색인되고 저장되는지 정의하는 부분이다.
- JSON 문서의 각 필드를 어떤방식으로 분석하고 색인할지, 어떤 타입으로 저장할지 등을 세부적으로 지정할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
### 데이터 생성
PUT /my_index2/_doc/1
Host: localhost:9200
Content-Type: application/json
{
"title": "Hello world",
"views": 1234,
"public": true,
"point": 4.5,
"created" : "2023-11-18T20:00:00.000Z"
}
### 인덱스 조회
GET /my_index2
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
{
"my_index2": {
"aliases": {},
"mappings": {
"properties": {
"created": {
"type": "date"
},
"point": {
"type": "float"
},
"public": {
"type": "boolean"
},
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"views": {
"type": "long"
}
}
},
"settings": {
}
}
}
- 이전에 없던 mappings 항목 밑에 각 필드의 타입과 관련된 정보가 새로 생긴 것을 확인할 수 있다.
3.2.1 동적 매핑 vs. 명시적 매핑
- ES가 자동으로 생성하는 매핑을 동적 매핑(Dynamic Mapping)이라고 부른다.
- 반대로 사용자가 직접 맵핑을 지정해주는 방법을 명시적 맵핑(Explicit Mapping)이라고 부른다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PUT /mapping_test
Host: localhost:9200
Content-Type: application/json
{
"mappings": {
"properties": {
"createdDate":{
"type": "date",
"format": "strict_date_time || epoch_millis"
},
"keywordString": {
"type": "keyword"
},
"textString": {
"type": "text"
}
}
},
"settings": {
"index.number_of_shards": 1,
"index.number_of_replicas": 1
}
}
- 각 필드에 데이터를 어떠한 형태로 저장할 것인지 타입이라는 설정으로 지정했다.
- 중요한 것은 필드 타입을 포함한 매핑 설정 내 대부분의 내용은 한 번 지정되면 사실상 변경이 불가능하므로 서비스 설계와 데이터 설계를 할때는 매우 신중하게 해야 한다.
- 위와 같은 이유로 서비스 운영 환경에서 대용량의 데이터를 처리해야 할 때는 기본적으로 명시된 매핑을 지정해서 인덱스를 운영해야 한다.
- 이 매핑을 어떻게 하느냐에 따라 서비스 운영 양상이 많이 달라지고, 성능의 차이도 크다.
맵핑 신규 필드 추가
- 신규 필드 추가가 예정돼 있다면 동적 매핑에 기대지 말고 명시적으로 매핑을 지정하는것이 좋다.
- 다음과 같이 신규 필드 매핑 정보를 추가할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
### 3.2.1 맵핑 신규 필드 추가
PUT /mapping_test/_mapping
Host: localhost:9200
Content-Type: application/json
{
"properties": {
"longValue": {
"type": "long"
}
}
}
맵핑 조회
1
2
3
### 3.2.1 맵핑 조회
GET /mapping_test/_mapping
Host: localhost:9200
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"mapping_test": {
"mappings": {
"properties": {
"createdDate": {
"type": "date",
"format": "strict_date_time || epoch_millis"
},
"keywordString": {
"type": "keyword"
},
"longValue": {
"type": "long"
},
"textString": {
"type": "text"
}
}
}
}
}
3.2.2 필드 타입
- 매핑의 기본은 필드 타입이다.
- 매핑의 필드 타입은 한번 지정되면 변경이 불가능하므로 매우 신중하게 지정해야 한다.
심플 타입
text
,keyword
,date
,long
,double
,boolean
,ip
등- 주로 직관적으로 알기 쉬운 간단한 자료형이다.
숫자 타입
- ES에서 지원하는 숫자 타입은 다음과 같다.
- long, integer, short, byte, double, float, half_float, scaled_float
- 작은 비트를 사용하는 자료형을 고르면 색인과 검색시 이득이 있다. 다만 저장할 때는 실제 값에 맞춰 최적화되기 때문에 디스크 사용량에는 이득이 없다.
- 부동소수점 수를 담는 필드라면 일반적으로 scaled_float 타입을 고려할 수 있다.
- 고정 환산 계수로 스케일링하고, long으로 저장되는데, 이를 사용하는 경우 정확도에서 손해를 보는 만큼 디스크를 절약할수 있다.
date 타입
1
2
3
4
"createdDate":{
"type": "date",
"format": "strict_date_time || epoch_millis"
}
- 필드 맵핑에 정의한 내용을 다시 살펴보면 2가지 format을 지원하므로 2가지를 모두 저장할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
### 3.2.2 dateType
PUT /mapping_test/_doc/1
Host: localhost:9200
Content-Type: application/json
{
"createdDate": "2020-09-03T02:41:32.001Z"
}
### 3.2.2 dateType2
PUT /mapping_test/_doc/2
Host: localhost:9200
Content-Type: application/json
{
"createdDate": 1599068514123
}
- 위처럼 2개의 문서를 모두 추가 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET /mapping_test/_search
Host: localhost:9200
Content-Type: application/json
{
"query": {
"range": {
"createdDate": {
"gte": "2020-09-02T17:00:00.000Z",
"lte": "2020-09-03T03:00:00.000Z"
}
}
}
}
- 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
28
29
30
31
32
33
34
35
{
"took": 354,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 1.0,
"hits": [
{
"_index": "mapping_test",
"_id": "1",
"_score": 1.0,
"_source": {
"createdDate": "2020-09-03T02:41:32.001Z"
}
},
{
"_index": "mapping_test",
"_id": "2",
"_score": 1.0,
"_source": {
"createdDate": 1599068514123
}
}
]
}
}
- 같은 필드에 문자열의 형식으로 문서를 색인 요청하든지 epoch milliseconds 형식으로 요청하든지 상관없이 모두 성공적으로 ES에 색인되는것을 확인할 수 있다.
- format은 여러 형식으로 지정할 수 있으며, 문서가 어떤 형식으로 들어오더라도 ES 내부적으로는 UTC 시간대로 변환하는 과정을 거쳐 epoch millisec 형식의 long 숫자로 색인된다.
- 위 예제에 정의된 2가지 타입 외에 epoch_second, date_time, date_optional_time, strict_date_optional_time 등이 더 있다.
배열
- long 타입 필드에는 309라는 단일 숫자 데이터를 넣을수도 있고, [221, 309, 1599208568] 과 같은 배열 데이터도 넣을수 있다.
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
### 인덱스 생성
PUT /array_test
Host: localhost:9200
Content-Type: application/json
{
"mappings": {
"properties": {
"longField": {
"type": "long"
},
"keywordField": {
"type": "keyword"
}
}
}
}
### 배열 인덱스 doc 추가
PUT /array_test/_doc/1
Host: localhost:9200
Content-Type: application/json
{
"longField": 309,
"keywordField": ["hello", "world"]
}
### 배열 인덱스 doc 추가2
PUT /array_test/_doc/2
Host: localhost:9200
Content-Type: application/json
{
"longField": [221, 309, 1599208568],
"keywordField": "hello"
}
- 문서들을 색인해보면 모두 성공적으로 색인이 되는것을 알수 있다.
실패하는 케이스( 타입을 다르게 하는 경우)
1
2
3
4
5
6
7
8
### 3.2.2 배열 인덱스 doc 추가 실패
PUT /array_test/_doc/2
Host: localhost:9200
Content-Type: application/json
{
"longField": [221, "hello"]
}
term
을 통해 문서 내 지정한 필드의 값 질의
1
2
3
4
5
6
7
8
9
10
11
12
### 3.2.2 배열 인덱스 조회
GET /array_test/_search
Host: localhost:9200
Content-Type: application/json
{
"query": {
"term": {
"longField": 309
}
}
}
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
{
"took": 3,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 1.0,
"hits": [
{
"_index": "array_test",
"_id": "1",
"_score": 1.0,
"_source": {
"longField": 309,
"keywordField": [
"hello",
"world"
]
}
},
{
"_index": "array_test",
"_id": "2",
"_score": 1.0,
"_source": {
"longField": [
221,
309,
1599208568
],
"keywordField": "hello"
}
}
]
}
}
- 조회 결과를 보면 단일데이터, 배열데이터에 관계 없이 모두 검색 결과에 포함된것을 알수 있는데, 이는 ES 색인 과정에서 데이터가 각 값마다 하나의 독립적인 역색인을 구성하기 떄문이다.
계층 구조를 지원하는 타입
- 필드 하위에 다른 필드가 들어가는 계층 구조의 데이터를 담는 타입으로 ojbect와 nested가 있다.
- 위 2가지가 유사하지만, 배열을 처리할 때는 동작이 다르다.
object 타입
- JSON 문서는 필드의 하위에 다른 필드를 여럿 포함하는 객체 데이터를 담을수 있다.
- object 타입은 이러한 형태의 데이터를 담는 필드 타입이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
### object 타입 인덱스 doc 추가
PUT /object_test/_doc/1
Host: localhost:9200
Content-Type: application/json
{
"price": 27770.75,
"spec": {
"cores":12,
"memory": 128,
"storage": 8000
}
}
### object 타입 인덱스 mapping 조회
GET /object_test/_mappings
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
{
"object_test": {
"mappings": {
"properties": {
"price": {
"type": "float"
},
"spec": {
"properties": {
"cores": {
"type": "long"
},
"memory": {
"type": "long"
},
"storage": {
"type": "long"
}
}
}
}
}
}
}
- 응답을 살펴보면 spec 필드의 타입을 명시적으로 ojbejct라고 표현하지는 않았는데, 이는 기본값이 object 타입이기 떄문이다.
object와 nested 타입의 배열 처리시 다른 동작 확인
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
### object 타입 배열로 색인
PUT /object_test/_doc/2
Host: localhost:9200
Content-Type: application/json
{
"spec": [
{
"cores":12,
"memory": 128,
"storage": 8000
},
{
"cores":6,
"memory": 64,
"storage": 8000
},
{
"cores":6,
"memory": 32,
"storage": 4000
}
]
}
### object 타입 배열에 대한 색인 확인
GET /object_test/_search
Host: localhost:9200
Content-Type: application/json
{
"query": {
"bool": {
"must": [
{
"term": {
"spec.cores": "6"
}
},
{
"term": {
"spec.memory": "128"
}
}
]
}
}
}
bool
쿼리의must
절에 쿼리를 여러 개 넣을 경우 각 쿼리가 AND조건으로 연결된다.- 위 조건대로라면 이 두 조건을 동시에 만족하는 객체는 존재하지 않으므로 예상대로라면 검색 결과는 비어야 한다. 하지만, 결과는 다르다.
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
{
"took": 970,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 2.0,
"hits": [
{
"_index": "object_test",
"_id": "2",
"_score": 2.0,
"_source": {
"spec": [
{
"cores": 12,
"memory": 128,
"storage": 8000
},
{
"cores": 6,
"memory": 64,
"storage": 8000
},
{
"cores": 6,
"memory": 32,
"storage": 4000
}
]
}
}
]
}
}
- 위 검색 결과는 object 타입의 데이터가 어떻게 평탄화되는지를 상기해보면 이해할 수 있다.
- 위 문서는 내부적으로 다음과 같은 형태로 데이터 평탄화가 된다.
1
2
3
4
5
{
"spec.cores": [12, 6, 6],
"spec.memory": [128, 64, 32],
"spec.cores": [8000, 8000, 4000]
}
- object 타입의 배열은 배열을 구성하는 객체 데이터를 서로 독립적인 데이터로 취급하지 않는다.
- 만약 이들을 독립적인 데이터로 취급하기를 원한다면
nested
타입을 사용해야 한다.
nested 타입
- nested 타입은 ojbect 타입과느 ㄴ다르게 배열 내 각 객체를 독립적으로 취급한다.
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
### nested type index 추가
PUT /nested_test
Host: localhost:9200
Content-Type: application/json
{
"mappings": {
"properties": {
"spec":{
"type": "nested",
"properties": {
"cores": {
"type": "long"
},
"memory": {
"type": "long"
},
"storage": {
"type": "long"
}
}
}
}
}
}
### nested type doc 추가
PUT /nested_test/_doc/1
Host: localhost:9200
Content-Type: application/json
{
"spec": [
{
"cores":12,
"memory": 128,
"storage": 8000
},
{
"cores":6,
"memory": 64,
"storage": 8000
},
{
"cores":6,
"memory": 32,
"storage": 4000
}
]
}
### nested 타입 search
GET /nested_test/_search
Host: localhost:9200
Content-Type: application/json
{
"query": {
"bool": {
"must": [
{
"term": {
"spec.cores": "6"
}
},
{
"term": {
"spec.memory": "128"
}
}
]
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"took": 352,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 0,
"relation": "eq"
},
"max_score": null,
"hits": []
}
}
- object 타입과 동일한 검색쿼리로 조회했지만 결과는 나오지 않는다.
- nested 타입은 객체 배열의 각 객체를 내부적으로 별도의 루씬 문서로 분리해 저장한다.
- 만약 배열의 원소가 100개라면 부모 문서까지 해서 101개의 문서가 내부적으로 생성된다.
- 이런 nested의 동작은 ES 내에서 굉장히 특수하기 때문에 nested 쿼리라는 전용 쿼리를 이용해 검색해야 한다.
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
### nested 전용 쿼리 search
GET /nested_test/_search
Host: localhost:9200
Content-Type: application/json
{
"query": {
"nested": {
"path": "spec",
"query": {
"bool": {
"must": [
{
"term": {
"spec.cores": "6"
}
},
{
"term": {
"spec.memory": "64"
}
}
]
}
}
}
}
}
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
{
"took": 4,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 2.0,
"hits": [
{
"_index": "nested_test",
"_id": "1",
"_score": 2.0,
"_source": {
"spec": [
{
"cores": 12,
"memory": 128,
"storage": 8000
},
{
"cores": 6,
"memory": 64,
"storage": 8000
},
{
"cores": 6,
"memory": 32,
"storage": 4000
}
]
}
}
]
}
}
spec.memory
값을 64로 지정하면 검색에 걸리지만, 128로 지정하면 걸리지 않는다.
nested 타입의 성능 문제
- nested 타입은 내부적으로 각 객체를 별도의 문서로 분리해서 저장하기 때문에 성능 문제가 있을수 있다.
- ES에서는 무분별한 nested 타입의 사용을 막기 위해 인덱스 설정으로 2가지 제한을 걸어 놓았다.
index.mapping.netsed_fields.limit
: nested 타입을 몇개까지 지어할수 있는지 제한(default 50)index.mapping._nested_objects.limit
: 한 문서가 nested 객체를 몇 개 까지 가질수 있는지 제한(default 10,000)- 위 값들을 무리하게 높이면 OOM의 위험이 있음.
object vs nested 간단 비교
타입 | object | nested |
---|---|---|
용도 | 일반적인 계층 구조에 사용 | 배열 내 각 객체를 독립적으로 취급해야 하는 특수한 상황에서 사용 |
성능 | 상대적으로 가벼움. | 상대적으로 무겁다. 내부적으로 숨겨진 문서가 생성됨 |
검색 | 일반적인 쿼리를 사용 | 전용 nested 쿼리로 감싸서 사용해야 한다. |
그 외 타입
- ES 인덱스의 필드 탕비에는 이 외에도 다양한 비즈니스 요구사항을 위한 타입들이 있다.
- 그 중에 몇가지만 소개하자면 다음과 같다.
종류 | 설명 |
---|---|
geo_point | 위도와 경도를 저장 |
geo_shape | 지도상에 특정 지점이나 선, 도형 등을 표현하는 타입 |
binary | base64로 인코딩된 문자열을 저장하는 타입 |
long_rage, date_range, ip_range 등 | 경곗값을 지정하는 방법 등을 통해 수, 날짜, IP 등의 범위를 저장 |
completion | 자동완성 검색을 위한 특수한 타입 |
text 타입과 keyword 타입
- 문자열 자료형을 담는 필드는 text와 keyword 타입 중 하나를 선택해 적용할 수 있다.
text
- text로 지정된 필드 값은 애널라이저가 적용된 후 색인된다. 이는 문자열 값 그대로를 가지고 역색인을 구성하는 것이 아니라 값을 분석하여 여러 토큰으로 쪼개고, 토큰으로 역색인을 구성한다.
term
: 쪼개진 토큰에 지정한 필터를 적용하는 등의 후처리 작업 후 역색인이 들어가는 형태를 말한다.
keywrod
- keyword로 지정된 필드에 들어온 문자열 값은 토큰으로 쪼개지 않고, 역색인을 구성한다.
- 애널라이저가로 분석하는 대신 노멀라이저를 적용한다.
- 노멀라이저는 간단한 전처리만을 거친 뒤 커다란 단일 term으로 역색인을 구성한다.
text와 keyword의 차이점
1
2
3
4
5
6
7
8
9
### doc 추가
PUT /mapping_test/_doc/3
Host: localhost:9200
Content-Type: application/json
{
"keywordString" : "Hello, World!",
"textString" : "Hello, World!"
}
1
2
3
4
5
6
7
8
9
10
11
12
### textString 검색
GET /mapping_test/_search
Host: localhost:9200
Content-Type: application/json
{
"query": {
"match": {
"textString": "hello"
}
}
}
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
{
"took": 3,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 0.18232156,
"hits": [
{
"_index": "mapping_test",
"_id": "3",
"_score": 0.18232156,
"_source": {
"keywordString": "Hello, World!",
"textString": "Hello, World!"
}
}
]
}
}
- textString으로 조회시 추가한 문서가 잘 조회되는것을 알수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
### keywordString 검색
GET /mapping_test/_search
Host: localhost:9200
Content-Type: application/json
{
"query": {
"match": {
"keywordString": "hello"
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 0,
"relation": "eq"
},
"max_score": null,
"hits": []
}
}
- 반면, keywordString으로 검색시에는 조회되지 않는것을 확인할 수 있다.
- 위 설명처럼 keywordString으로 문서를 찾고싶다면 정확히 일치하는 문자열로 검색을 하면 정상적으로 결과를 얻을수 있다.
- text 타입과 keyword 타입은 색인 과정에서도 위에 보이는것처럼 각각 애널라이저, 노멀라이저에 의해 색인이 이루어진다.
1
2
3
4
5
6
7
8
9
10
11
12
### textString match 검색
GET /mapping_test/_search
Host: localhost:9200
Content-Type: application/json
{
"query": {
"match": {
"textString": "THE WORLD SAID HELLO"
}
}
}
- 추가로 text 타입의 match 질의 과정을 살펴보면 match 질의 질의어도 쪼개 생성한 4개의 텀을 역색인에서 찾는다.
위 예제에서는 world 텀과 hello 텀을 역색인에서 찾을 수 있으므로 검색 결과에 색인한 문서가 조회된다.
- 위 색인 방식에서 알수 있듯이 text 타입은 주로 전문 검색에 적합하고, keyworc 타입은 일치 검색에 적합하다.
- 이 두 타입은 정렬과 집계, 스크립트 작업을 수행할 때에도 동작의 차이가 있다.
- 정렬과 집계, 스크립트 작업의 대상이 될 필드는 text 타입보다는 keyword 타입을 쓰는 편이 낫다.
- keyword 타입은 doc_values라는 캐시를 사용하고, text 타입은 fielddata라는 캐시를 사용하기 때문이다. 이와 관련된 내용은 이어서 설명한다.
3.2.3 doc_values
- ES의 검색은 역색인을 기반으로 한 색인을 이용한다.(텀을 보고 역색인에서 문서를 찾는 방식)
- 정렬, 집계, 스크립트 작업시에는 접근법이 다르다.
- doc_values는 디스크를 기반으로 한 자료구조로 파일 시스템 캐시를 통해 효율적으로 정렬, 집계, 스크립트 작업을 수행할수 있도록 설계되었다.
- ES에서는 text와 annotated_text 타입을 제외한 거의 모든 필드 타입이 doc_values를 지원한다.
- doc_valuesㄱ를 끄고싶다면 맵핑 지정시 아래와 같인 속성값을 false로 할 수 있다.(doc_values가 지원되는 타입의 기본값은 true)
1
2
3
4
5
6
7
8
9
10
11
12
13
### doc_values mappaing
PUT /mapping_test/_mapping
Host: localhost:9200
Content-Type: application/json
{
"properties": {
"notForSort":{
"type": "keyword",
"doc_values": false
}
}
}
3.2.4 fielddata
- text 타입의 경우 파일 시스템 기반의 캐시인 doc_values를 사용할 수 없다.
- text 필드를 대상을 정렬, 집계, 스크립트 작업을 수행할 때에는 fielddata라는 캐시를 이용한다.
- fielddata를 사용한 정렬이나 집계 등의 작업시에는 역색인 전체를 읽어들여 힙 메모리에 올려서 처리한다.
- 이런 동작 방식은 힙을 순식간에 차지해 OOM 등 많은 문제가 발생할 수 있어 기본값은 비활성화 상태이다.
- 특별한 이유가 있는 경우 이를 활성화해야 한다면 다음과 같이 mapping properties 를 지정해줄수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
### fielddata mappaing
PUT /mapping_test/_mapping
Host: localhost:9200
Content-Type: application/json
{
"properties": {
"sortableText":{
"type": "text",
"fielddata": true
}
}
}
fielddata를 사용하는 경우 주의사항
- text 필드의 fielddata를 활성화 하는 것은 매우 신중해야 한다.
- “Hello, World!”의 역색인 텀은 이미 분석이 완료된 “hello”와 “world” 텀인데, 이를 이용해서 집계를 수행하기 떄문에 의도와 다른 결과가 나올수 있다.
- 무거운 작업으로 OOM이 발생하면 클러스터 전체에 장애를 불어올수도 있다.
doc_values
vs fielddata
doc_values | fielddata | |
---|---|---|
적용 타입 | text, annotated_text를 제외한 모든 타입 | text, annotated_text |
동작 방식 | 디스크 기반이며 파일 시스템 캐시를 활용한다. | 메모리에 역색인 내용 전체를 올린다.(OOM 유의) |
기본값 | 기본적으로 활성화 | 기본적으로 비활성화 |
text
vs keyword
text | keyword | |
---|---|---|
분석 | 애널라이저로 분석하여 여러 토큰으로 쪼개진 텀을 역색인 | 노멀라이저로 전처리한 단일 텀을 역색인 |
검색 | 전문 검색에 적합 | 단순 완전 일치 검색에 적합 |
정렬, 집계, 스크립트 | fielddata를 사용하므로 적합하지 않음. | doc_values를 사용하므로 적합함. |
3.2.5 _source
_source
필드는 문서 색인 시점에 ES에 전달된 원본 JSON 문서를 저장하는 메타데이터 필드- _source 필드는 JSON 문서를 통째로 담기 때문에 디스크를 많이 사용한다.
_source 비활성화
- 아래처럼 mappings에 설정을 통해 메타 데이터를 저장하지 않도록 설정할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
### _source
PUT /no_source_test
Host: localhost:9200
Content-Type: application/json
{
"mappings": {
"_source": {
"enabled": false
}
}
}
_source 비활성화할 경우 발생할수 있는 문제
- update와 updated_by_query API를 이용할 수 없다.
- ES의 소개에서 이야기한것처럼 ES 세그먼트는 불변인데, 기존 문서를 삭제하고 업데이트된 새 문서를 색인하는 과정에서 _source를 참조해야 하는데 update를 이용할 수 없다.
- reindex API를 사용할 수 없다.
- reindex는 원본 인덱스에서 내용을 대상 인덱스에 새로 색인하는 작업인데, 이떄에도 _source의 원본 JSON 데이터를 읽어 재색인 작업을 수행한다.
- reindex는 ES 운영과 데이터 관리에 있어 핵심 API이다.
- reindex를 못하는게 _source를 비활성화 할수 없는 충분한 이유가 된다.
- ES major 버전업시 문제가 될수 있다.
- 일반적으로 major version이 2 이상 차이나는 경우 읽을수 없음
- major 버전 업그레이드 전 reindex를 수행해 예쩐에 생성된 인덱스를 재생성해야 한다.
인덱스 코덱 변경
- 다른 성능을 희생하더라도 디스크 공간을 절약해야만 하는 상황이라면 _soruce의 비활성화보다는 차라리 인덱스 데이터의 압축률을 높이는 편이 낫다.
1
2
3
4
5
6
7
8
9
10
11
12
### index codec 변경
PUT /codec_test
Host: localhost:9200
Content-Type: application/json
{
"settings": {
"index": {
"codec": "best_compression"
}
}
}
- default (LZ4 압축) / best_compression( DEFLATE 압축) 중 1가지를 선택할 수 있다.
- 이 설정은 동적으로 변경이 불가능하다.
synthetic source
- ES 8.4 부터
synthetic source
라는 기능이 도입되었다. - 이를 사용하는 인덱스는 _source에 JSON 원문을 저장하지 않는데, _source 비활성화와 다른점은 _source를 읽어야 하는 떄가 되면 문서 내 각 필드의 doc_values를 모아 _source를 재조립해 동작하는 점이다.
- 인덱스의 모든 필드가 doc_values를 사용하는 필드여야 한다는 제약이 있고, source를 읽어야 하는 작업의 성능이 떨어질수 있다.
- 원문 JSON과 100% 일치하지는 않을수 있음(필드명, 배열내 순서 등)
1
2
3
4
5
6
7
8
9
10
11
12
### synthetic source 지정
PUT /synthetic source_test
Host: localhost:9200
Content-Type: application/json
{
"mappings": {
"_source": {
"mode": "synthetic"
}
}
}
_source 관련 설정 주의사항
_source
와 관련된 설정은 운영에 영향이 매우 크므로 특별한 경우에 한해 심사숙고하여 결정해야 한다.
3.2.6 index
- index 속성은 해당 필드의 역색인을 만들것인지를 지정한다.(기본값 true)
- false로 설정하면 해당 필드는 역색인이 없기 떄문에 일반적인 검색 대상이 되지 않는다.
- 역색인을 생성하지 않는 것뿐이기 때문에 doc_values를 사용하는 타입의 필드라면 정렬이나 집계의 대상으로는 사용할 수 있다.
- ES 8.1 이상부터는 index 속성을 false로 설정하는것만으로 검색 대상에서 제외되지는 않는다.
- doc_values를 사용하는 필드 타입의 경우 index가 false이더라도 역색인 대신 doc_values를 이용해 검색을 수행한다.
- 다만, doc_values는 검색을 위해 설계된 자료형이 아니므로 검색 성능이 많이 떨어진다.
- 데이터 설계나 서비스 설계상 검색 대상이 될 가능성이 없거나 검색 대상이 되더라도 관리적인 목적으로 아주 가끔 소규모의 검색만 필요한 경우 index 값을 false로 지정한 성능&저장공간상 이득을 볼수 있다.
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
### no index
PUT /mapping_test/_mapping
Host: localhost:9200
Content-Type: application/json
{
"properties": {
"notSearchableText": {
"type": "text",
"index": false
},
"docValueSearchableText": {
"type": "keyword",
"index": false
}
}
}
### no index doc add
PUT /mapping_test/_doc/4
Host: localhost:9200
Content-Type: application/json
{
"textString": "Hello, World!",
"notSearchableText": "World, Hello!",
"docValueSearchableText": "hello"
}
### no index search error
GET /mapping_test/_search
Host: localhost:9200
Content-Type: application/json
{
"query": {
"match": {
"notSearchableText": "hello"
}
}
}
- 위와 같이 검색하게 되면 index가 없어서 검색이 불가능하다는 응답을 받게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
### no index doc_values search
GET /mapping_test/_search
Host: localhost:9200
Content-Type: application/json
{
"query": {
"match": {
"docValueSearchableText": "hello"
}
}
}
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
{
"took": 4,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 1.0,
"hits": [
{
"_index": "mapping_test",
"_id": "4",
"_score": 1.0,
"_source": {
"textString": "Hello, World!",
"notSearchableText": "World, Hello!",
"docValueSearchableText": "hello"
}
}
]
}
}
- 반면, docValueSearchableText 를 이용하면 index가 없음에도 조회되는것을 알수 있다.
3.2.7 enabled
- enabled 설정은 object 타입의 필드에만 적용된다.
- enabled가 false로 지정된 필드는 ES가 파싱조차 수행하지 않는다.
- _source에는 데이터가 있으나, 다른 어느곳에도 저장되지 않는다.
- 역색인을 생성하기 않으므로 검색도 불가능하다.
- 정렬이나 집계의 대상도 될수 없다.
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
### endabled mapping
PUT /mapping_test/_mapping
Host: localhost:9200
Content-Type: application/json
{
"properties": {
"notEnabled": {
"type": "object",
"enabled": false
}
}
}
### endabled doc add
PUT /mapping_test/_doc/5
Host: localhost:9200
Content-Type: application/json
{
"notEnabled": {
"mixedTypeArray": [
"hello",
4,
false,
{"foo": "bar"},
null,
[2, "E"]
]
}
}
### endabled doc add2
PUT /mapping_test/_doc/6
Host: localhost:9200
Content-Type: application/json
{
"notEnabled": "world"
}
- 서비스 설계쌍 최종적으로 데이터를 _source에서 확인만 하면 되고, 그 외 어떤 활용도 필요치 않은 필드가 있다면 enabled를 false로 지정하는것을 고려해볼 수 있다.
Reference
- .
Comments powered by Disqus.