Home [엘라스틱서치 바이블] 3. 인덱스 설계 - 1
Post
Cancel

[엘라스틱서치 바이블] 3. 인덱스 설계 - 1

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 간단 비교
타입objectnested
용도일반적인 계층 구조에 사용배열 내 각 객체를 독립적으로 취급해야 하는 특수한 상황에서 사용
성능상대적으로 가벼움.상대적으로 무겁다. 내부적으로 숨겨진 문서가 생성됨
검색일반적인 쿼리를 사용전용 nested 쿼리로 감싸서 사용해야 한다.

그 외 타입

  • ES 인덱스의 필드 탕비에는 이 외에도 다양한 비즈니스 요구사항을 위한 타입들이 있다.
  • 그 중에 몇가지만 소개하자면 다음과 같다.
종류설명
geo_point위도와 경도를 저장
geo_shape지도상에 특정 지점이나 선, 도형 등을 표현하는 타입
binarybase64로 인코딩된 문자열을 저장하는 타입
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으로 문서를 찾고싶다면 정확히 일치하는 문자열로 검색을 하면 정상적으로 결과를 얻을수 있다.

스크린샷 2023-11-19 오후 4 45 39

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

스크린샷 2023-11-19 오후 4 49 45

  • 추가로 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_valuesfielddata
적용 타입text, annotated_text를 제외한 모든 타입text, annotated_text
동작 방식디스크 기반이며 파일 시스템 캐시를 활용한다.메모리에 역색인 내용 전체를 올린다.(OOM 유의)
기본값기본적으로 활성화기본적으로 비활성화

text vs keyword

 textkeyword
분석애널라이저로 분석하여 여러 토큰으로 쪼개진 텀을 역색인노멀라이저로 전처리한 단일 텀을 역색인
검색전문 검색에 적합단순 완전 일치 검색에 적합
정렬, 집계, 스크립트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

  • .
This post is licensed under CC BY 4.0 by the author.

[엘라스틱서치 바이블] 2. 엘라스틱 서치 기본 동작과 구조

[엘라스틱서치 바이블] 3. 인덱스 설계 -2

Comments powered by Disqus.