Mysql 인덱스 사용법 및 실행 계획 정리

데이터베이스/mysql|2020. 6. 10. 19:18

mysql 인덱스에 대한 정확한 이해도 없이 사용을 하다보니 조금 개념적으로 헷갈리는게 많이 있었다. 이 부분에 대해 한번 정리하고 넘어가고자 기록해본다.

 

인덱스


인덱스는 빠르게 특별한 컬럼과 함께 값을 찾는데 사용된다. 인덱스가 없으면 Mysql은 처음 행부터 전체 테이블을 읽어 들여서 데이터를 찾는다. 거대한 테이블에서 이런 행동은 비용이 상당히 많이 들어가게 된다. 만약에 테이블이 인덱스를 가지고 있으면 빠르게 접근할 수 있게 된다.

대부분의 Mysql 인덱스 (PRIMARY KEY, UNIQUE, INDEX, and FULLTEXT)는 B-tree안에 저장된다. 예외적으로 spatial 데이터 타입은 R-tree를 사용, 메모리 테이블은 또한 hash index를 지원, InnoDB는 FULLTEXT 인덱스를 위해 inverted list를 사용한다.

 

 

인덱스 동작 방식


- 행을 찾기 위해서 매칭되는 WHERE 구문을 빠르게 찾는다.

 

- 조건으로 부터 불필요 행을 제거한다. 만약에 여러 인덱스가 있는 경우 Mysql은 가장 적은 수의 행을 사용하는 인덱스를 선택한다. (Mysql은 한번에 하나의 인덱스만 사용할 수 있다.)

 

- 만약 테이블이 multiple column 인덱스를 가지고 있으면 인덱스의 가장 왼쪽에 컬럼을 사용하여 옵티마이저를 통해 행을 찾는다. 예를 들어 만약에 (col1, col2, col3)을 사용하는 인덱스가 있는 경우 인덱스는 이 순서로 검색을 진행한다. (col1), (col1, col2), (col1, col2, col3)

 

- 조인이 있는 경우 다른 테이블에서 행을 찾는다. Mysql은 동일한 유형과 사이즈로 되어 있는 열을 index로 사용할 때 더욱 효과적으로 행을 찾는다. VARCHAR, CHAR는 두개를 같은 사이즈로 명시 하였을 경우에 같은 타입으로 고려되어 사용된다. 예를 들어 VARCHAR(10) = CHAR(10)이지만 VARCHAR(10) ≠ CHAR(15)는 같지 않다.

 

- binary가 아닌 문자열 사이를 비교하기 위해서는 동일한 문자열 집합을 사용해야한다. utf8과 latin1열을 비교할 경우 인덱스를 사용할 수 없다.

 

- 타입이 다른 문자열과 숫자 등을 비교하려고 할때도 마찬가지로 인덱스를 탈 수 없다. (묵시적 형변환)

 

- index로 사용되는 key_col에서 min(), max()의 값을 찾기 위해 인덱스에서 key_col 이전에 발생하는 모든 키 파트에서 WHERE key_part_n = const를 사용하는지 여부를 확인하기 위해서 전처리기를 통해서 최적화가 진행된다. 이 경우에서 Mysql은 각 min() 또는 max() 표현식에 대해 단일키 조회를 수행하여 상수를 대체한다. 모든 표현식이 상수로 바뀌고 나면 쿼리가 한번에 반환된다.

SELECT MIN(key_part2),MAX(key_part2)
  FROM tbl_name WHERE key_part1=10;

 

 

실행계획


- 실행계획 필드 정리

  • Id
    • Select 구문 구분 ID
  • select_type
    • SIMPLE : 단순 SELECT
    • DERIVED : 서브 쿼리 중 가장 안쪽에 있는 쿼리 
    • PRIMARY : 서브 쿼리 바깥쪽에 있는 쿼리
    • DEPENDENT SUBQUERY  : 조건절 내부에서 외부 쿼리와 연결된 SELECT 
// PRIMARY
SELECT * FROM ( 
    // DERIVED : 서브 쿼리 중 가장 안쪽에 있는 쿼리
    SELECT * FROM timeline_item ti
) tt;


// DEPENDENT SUBQUERY
SELECT * FROM timeline_item t1 WHERE EXISTS ( SELECT * FROM timeline_site t2 WHERE t1.id = t2.id)
  • table

    • 참조하는 테이블 이름
  • type

    • 조인 혹은 조회 타입 (아래로 갈수록 성능 하락)
      1. System : 테이블에 데이터가 하나만 있는 경우
      2. const : SELECT에서 Primary Key 혹은 Unique Key를 상수로 조회하는 경우
      3. eq_ref : 조인할 때 Primary, Unique Key로 매칭하는 경우
      4. ref : 조인할 때 Primary, Unique Key로 매칭하지 않은 경우
      5. ref_or_null : ref와 같지만 NULL이 추가되어 검색된 경우
      6. index_merge : 두개의 인덱스가 병합되어 검색이 된경우
      7. unique_subquery : In절 내부 서브쿼리에서 Primary Key가 있는 경우
      8. index_subquery : In절 내부 서브쿼리에서 Primary Key가 아닌 인덱스가 있는 경우
      9. range : 특정 범위 내에서 인덱스를 사용하여 데이터 추출 하는 경우
      10. index : 인덱스를 처음부터 끝까지 찾아서 검색하는 경우로 일반적인 인덱스 풀스캔
      11. all : 테이블 풀스캔
  • possible_keys

    • 데이터 조회 시 DB에서 사용할 수 있는 인덱스 리스트
  • key

    • 실제로 사용할 인덱스
  • key_len

    • 실제로 사용할 인덱스 길이
  • ref

    • key 안의 인덱스와 비교하는 컬럼(상수)
  • rows

    • 쿼리 실행 시 조사하는 행수
  • extra

    • 추가 정보 (데이터가 많고 Using filesort, Using temporary 상태가 나온다면 무조건 최적화 필요)

      1. Using Index

        커버링 인덱스라고 하며 인덱스 자료구조를 이용하여 데이터를 추출

      2. Using where

        where 조건으로 데이터를 추출 (Type이 All 또는 Index와 같이 표현될 시 성능이 안좋다는 뜻)

      3. Using filesort

        데이터 정렬이 필요한 경우로, 메모리 혹은 디스크 상에서의 정렬을 모두 포함 (데이터 많을 시 성능 하락)

      4. Using Temporary

        쿼리 처리 시 내부적으로 Temporary 테이블이 사용됨

 

 

WHERE 조건문 주의사항


  • 묵시적 형변환에 조심하라
  • 무턱되고 함수를 사용하면 옵티마이저가 데이터 분포도 체크를 하지 못하기에 사용하지 말 것
// 함수 사용
SELECT * FROM timeline_item ti WHERE DATE_FORMAT(modified_at, '%Y%m%d') <= '20200402'

// 대안방안
SELECT * FROM timeline_item ti WHERE modified_at <= '2020-04-02'
  • Like 검색은 % 위치에 따라 다르게 수행 된다. %123, %125%의 경우에는 데이터 풀 스캔이 발생된다. 하지만 12312%와 같은 경우에는 인덱스를 사용해서 진행됨. 하지만 1%등과 같이 데이터 분포도에 비해 너무 추상적으로 %를 사용하게 될 경우에는 옵티마이저가 인덱스 사용과 데이터 풀스캔의 효율성 판단 했을 때 풀스캔이 효율적이다고 생각하고 데이터 풀스캔이 실행된다. 

댓글()

Elasticsearch 7.7 feature와 heap 메모리 사용량의 두드러진 감소량

 


줄어든 heap 사용량

Elasticsearch 사용자들은 Elasticsearch 노드에 저장이 가능한 만큼 데이터를 집어 넣지만, 가끔 disk에 저장되기 전에 heap memory 사용량이 초과되는 것을 경험한다. 이는 비용을 줄이기 위해 가능한 노드당 많은 양의 데이터를 넣고 싶은 사용자들에게 문제를 일으킨다. (실제로 현재 운영중인 es에서도 대량의 데이터 삽입 시 가끔 발생함)

 

왜 Elasticsearch에는 데이터를 저장하기 위해 heap memory 영역이 필요한걸까? 왜 디스크 공간만으로 충분하지 않은걸까?? 거기에는 여러 이유가 존재하지만 가장 중요한 이유는 루씬은 디스크 상에 데이터를 찾을 수 있는 위치를 찾아내기 위해서 일부 정보를 메모리에 저장해야 한다.

 

예를 들어 루씬의 inverted index는 terms 사전(디스크 상에 순서대로 블록 형태로 되어있는 terms group)과 terms index(terms 사전에서 빠르게 조회하기 위해 구성된)로 구성되어 있다. 이 terms index는 디스크상의 블록에 prefix starts 위치를 포함하고 있는 terms를 offset과 함께 terms의 prefix 정보로 도식화 하고 있다. 그런데 이 terms 사전은 disk 상에 존재하지만 terms index는 heap 위에서 존재한다.

 

그럼 얼마나 많은 양의 메모리가 필요로 할까? 전형적으로 인덱스 GB당 작은 MB 만큼이 필요로 한다. 이것은 많지는 않지만 사용자가 노드에 terabyte 상당의 데이터를 디스크에 사용한다면 indicies는 indices에 terabyte만큼의 데이터를 저장하기 위해서 10~20GB상당의 heap memory가 필요로 하게 된다.

 

Elasticsearch에서는 30GB이상의 힙메모리를 올리지 말라고는 하지만 종종 집계와 같은 쿼리 시 다른 consumer를 위한 공간을 남기지 않기 때문에 JVM에서 클러스터 관리 작업을 위한 공간이 충분치 않는 경우가 많아 운영에 어려움을 주는 경우가 있다.

 

실제로 기존에 6.x 버전과 7.x 초기버전의 경우에는 10TB 데이터 저장 시 17기가의 힙 메모리가 필요로 했다. 하지만 7.7버전에서는 2.5기가만 필요로 하도록 개선되었다고 한다.

 

어떻게 이게 가능해진걸까? Jvm에서 디스크로 데이터를 옮기는 구조와 메모리에서 hot bits를 유지하기 위해서 파일시스템을 사용하는 등의 기술들이 루씬 indices의 여러 컴포넌트들에게 시간이 흐름에 따라 동일하게 적용되고 있다. 그리고 이 메모리는 여전히 할당된 곳에서 내용을 읽을 수는 있지만 이 메모리에 상당한 부분은 사용사례에 따라 사용 되지 않는 경우가 많았다.

 

예를 들어 디스크상의 _id field의 terms index의 이동으로 삭제된 terms는 오직 GET API와 정확한 IDS로 document들을 인덱싱 했을 때만 사용된다. 하지만 elasticsearch로 메트릭과 로그를 인덱스하는 사용자의 대부분은 해당 기능을 사용하지 않는다. 이렇게 사용되지 않고 있는 자원들을 활용해서 heap의 사용률을 7.7버전 부터는 더 적게 heap 크기를 사용 할 수 있게 되었다.

 

그 밖에 새로운 feature

이 밖에도 검색 결과를 동기로 기다리지 않고 검색결과를 검색 시 사용한 ID를 이용해서 추후해 결과를 얻을 수 있는 async search와 aggregation시 많은 bucket을 할 당할 경우 발생할 수 있는 OOM을 피하기 위해서 주기적으로 memory circuit breaker를 bucket을 추가 할당 하기 전에 체크하는 기능 등이 추가되었다.

 

 

 

 

출처 및 읽어보면 좋은 링크

 

인덱스와 샤드의 관계

https://www.elastic.co/kr/blog/how-many-shards-should-i-have-in-my-elasticsearch-cluster

How many shards should I have in my Elasticsearch cluster?

이 블로그는 여러분의 클러스터에 적합한 인덱스와 샤드의 개수와 크기를 어떻게 가져가야 하는지에 대한 실질적인 가이드라인을 제공합니다.

www.elastic.co

https://www.elastic.co/kr/blog/significantly-decrease-your-elasticsearch-heap-memory-usage

Significantly decrease your Elasticsearch heap memory usage

Fitting as much data per Elasticsearch node as possible is often important to reduce costs. Learn more about the improvements coming in Elasticsearch 7.7 to dramatically reduce the amount of heap memory needed per GB of data.

www.elastic.co

 

댓글()

Too many dynamic script compilations 에러

elasticsearch를 사용하여 개발을 하다보면 스크립트를 사용하는 경우가 굉장히 많이 발생한다. 나는 아무생각없이 스크립트를 만들어서 사용했는데 어느날 운영에 반영을 하는데 본적이 없던 에러를 발견했다.

 

java.lang.AssertionError: 
Expecting code not to raise a throwable but caught
  <"ElasticsearchStatusException[Elasticsearch exception [type=search_phase_execution_exception, reason=all shards failed]]; nested: ElasticsearchException[Elasticsearch exception [type=circuit_breaking_exception, reason=[script] Too many dynamic script compilations within, max: [75/5m]; please use indexed, or scripts with parameters instead; this limit can be changed by the [script.max_compilations_rate] setting]];

스크립트 컴파일 에러가 발생했다고 하는데 왜 발생한 것인지... 몰라 검색해봤다.

 

 

에러 원인

elasticsearch는 기본적으로 컴파일하여 사용할 수 있는 스크립트 수를 제한을 한다고 한다. 그 제한의 기본값은 75/5m rate 즉 5분동안 75개의 스크립트만 컴파일하여 사용이 가능하다고 한다.

 

그래서 그 이상의 스크립트를 컴파일 하려고 할 시 Elasticsearch에서 out of meory 방지를 위해 circuit을 열어버린다. 

https://www.elastic.co/guide/en/elasticsearch/reference/current/circuit-breaker.html

 

근데 나는 스크립트를 그 정도로 많이 만들어서 사용하지는 않았는데 어떻게 그럴수가 있을까 하고 있던 찰나에 같이 일하는 동료분께서 내가 짠 스크립트에서 문제가 있다고 확인해주셨다.

 

바로 데이터를 스크립트에 사용되는 유동적인 데이터 중 일부가 param의 형태로 들어가지 않고 스크립트를 만들 때 동적으로 들어가게 해놨던 것이었다....

 

예를 들면 다음과 같이 스크립트를 매번 만들었던 것이었다. ㅜㅜ 그래서 당연히 초당 요청이 엄청 많은 우리 서비스에서 75요청은 그냥 넘어갔고 그 결과 서킷이 열려버려서 elasticsearch에서 400에러를 내뱉었다.

for (int i = 0; i <= 76; i++) {
	testRepository.search(QueryBuilders.scriptQuery(new Script(ScriptType.INLINE, "painless", "return 0 <= " + i + ";", Collections.emptyMap())));
}

아뿔싸 나 때문에 오전부터 고생했던 배포를 못하게 되었다 ㅜㅜ

 

 

해결방법

이 문제를 해결하기 위한 방식은 2가지가 있다.

 

 

먼저 유동적으로 들어오는 데이터로인해 스크립트가 계속 새로 컴파일 되지 못하도록 유동성 데이터는 param으로 넘겨서 사용하는 방식이다. 나 또한 이 방식으로 데이터를 바꿨다. 위의 예시 기준으로 다음과 같이 변경하였다.

for (int i = 0; i <= 76; i++) {
	testRepository.search(QueryBuilders.scriptQuery(new Script(ScriptType.INLINE, "painless", "return 0 <= params[i];", Collections.emptyMap("i", i))));
}

 

 

그리고 또다른 방식으로는 실제로 스크립트가 많이 컴파일 되어야 할 때는 상황에 맞게 그 rate를 조절해야 할 수도 있다. 이때는 다음과 같이 변경해주면된다.

// dsl
PUT http://localhost:9200/_cluster/settings
{
  "transient": {
    "script.max_compilations_rate": "150/1m"
}

// java (rest high level client)
public void changeScriptMaxCompileRate(String rate) {

	ClusterUpdateSettingsRequest request = new ClusterUpdateSettingsRequest();
	request.transientSettings(ImmutableMap.of(
		"script.max_compilations_rate", rate
	));

	restHighLevelClient.cluster().putSettings(
		request, RequestOptions.DEFAULT
	);
}

 

하지만 이와 관련된 이슈에서 다음과 같은 문구를 찾았다. 

The best solution is actually to not to increase the limit. If a test suite breaks the amount of compilations allowed, it will absolutely blow up in any serious environments.

The best solution is to figure out which painless script(s) are always recompiling, and parameterize them instead. I've had that happen on a few occasions, and I just needed to move some literal values out of the script and into params. Check out this PR: https://github.com/elastic/beats/pull/9308/files#diff-759f580883147ab049f76cd3501ec965R32

 

무조건 limit를 늘리는건 방식이 아니고 재 컴파일 되지 않도록 수정하는게 옳은 방식이라고 한다.

물론 나 때문에 문제가 발생하였지만 몰랐고 놓쳤던 부분을 알게 되어서 값비싼 수업을 들은 기분이 들었다.

댓글()

Elasticsearch node 종류와 기본 설정 옵션

Elasticsearch의 노드 

Elasticsearch의 인스턴스를 시작하는 동시에 노드도 같이 시작된다. 노드들을 연결해놓은 것을 클러스터라고 한다.

만약 하나의 엘라스틱 서치 노드만을 실행시킨 경우도 하나의 노드를 가진 클러스터라고 한다. 

클러스터안에서 모든 노드는 HTTP와 Transport 트래픽을 기본적으로 다룬다. Transport 레이어는 오로지 노드들과 Java TransportClient와의 통신에만 사용된다. Http 레이어는 오직 외부 Rest Cliente들과 통신할 때 사용된다.

모든 노드는 클러스터 안에서 서로 다른 노드들에 대하여 알고 있고 client에 요청을 적적한 노드로 향하게 조절해준다. 기본적으로 노드는 master-eligible, data, ingest, machine learning이 존대한다.

 

Elasticsearch의 노드 종류

Master-eligible 노드

- node.master를 true로 지정하며 클러스터의 컨트롤을 통해 마스터 노드로 선택될 자격을 가지게 된다.

- 마스터 노드는 클러스터에서 인덱스를 만들고 지우는 행위, 클러스터에서 노드들을 트래킹하고 각각의 노드를 샤드를 할당할건지 결정한다.

- Masger Eligible Node등에서 마스터 노드는 마스터 설출 프로세스를 통해 선출된다.

(https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-discovery.html)

- 마스터 노드는 데이터 노드처럼 데이터와 폴더에 접근 권한이 있어야 한다. 노드가 재시작 하는 사이에도 클러스터 상태가 유지되어야 하기 때문에 접근이 가능해야 한다.

- 데이터를 인덱싱하고 찾고 하는 작업은 CPU, Memory, I/O 자원을 많이 사용하기 때문에 큰 규모에서는 data node와 master node를 구별한다.

- 마스터 노드도 Coordinating Node 처럼 데이터를 routing하고 모으고 하는 작업이 가능하지만 이는 마스터 노드가 하는 주 목적이 아니다. 안정적인 마스터 노드를 운영하기 위해서는 자기 일만 딱 하게 해주는 것이 좋다.

- 마스터 노드로 노드를 지정하기 위한 기본 설정 값은 다음과 같다.

node.master: true 
node.data: false 
node.ingest: false 
node.ml: false 
xpack.ml.enabled: true 
cluster.remote.connect: false

 

Data 노드

- node.data가 true로 지정된 노드는 데이터를 가지고 있을 수 있고 CRUD, 검색, aggregation 등의 데이터와 관련된 작업이 가능하다.

- 데이터 노드는 인덱싱 된 Document를 포함하고 있는 샤드를 관리한다.

- 데이터 노드는 데이터를 직접적으로 다루기 때문에 리소스 자원이 많이 필요하다.

- 데이터 노드로 노드를 지정하기 위한 기본 설정 값은 다음과 같다.

node.master: false 
node.data: true 
node.ingest: false 
node.ml: false 
cluster.remote.connect: false

 

Ingest 노드 

- node.ingest가 true로 지정된 노드가 Document가 인덱싱 되기 전에 변형되고 풍성하게 하기 위해서 Document를 ingest pipeline으로 적용할 수 있다.

- ingetst node는 pre processing 파이프라인을 실행하고 하나 또는 하나 이상의 ingest processor들을 모으는 작업을 한다.

- ingest를 로드하는건 무겁기 때문에 데이터나 마스터 노드에서는 node.ingest를 false로 지정하는 것이 좋다.

- 많은 리소스를 잡아먹기 때문에 ingest node는 별도로 지정하는 것이 좋다.

- ingest 노드로 노드를 지정하기 위한 기본 설정 값은 다음과 같다.

node.master: false 
node.data: false 
node.ingest: true 
node.ml: false 
cluster.remote.connect: false

 

Machine Learning 노드

- xpack.ml이 true로 지정되어 있고 node.ml이 true로 설정되어 있는 노드는 기본적으로 엘라스틱서치에서 분배하는 행위를 한다.

- 만약 머신러닝 특징을 사용하고 싶으면 적어도 클러스터 내에 하나의 머신러닝 노드가 있어야 한다.

 

Coordinating 노드

- 검색 요청과 bulk indexing과 같은 요청들은 다른 노드들의 있는 데이터를 많이 다룬다.

- 데이터가 흩어져 있는 경우 데이터가 있는 노드로 향하게 조정해준다. 각각의 데이터 노드는 요청을 자체적으로 처리하고 그것의 값을 Coordinating 노드에 전달해준다. 그럼 Coordinating 노드는 이를 모아서 하나의 데이터 형태로 정제하여 반환한다.

- 각각의 노드는 Coordinating node가 될 수 있다. 대신 node.master, node.data, node.integer가 false로 되어 있어야 한다.

- 또한 데이터를 모으고 조작하고 하는 작업이 많기 때문에 Coordinating 노드는 메모리랑 CPU에 대한 자원이 많아야 한다. 그렇기 때문에 오직 요청을 라우팅하고 검색 구절을 조절하고, bulk indexing 분배작업을 하는 노드로만 사용하는 게 좋다.

- Coordinating node는 본질적으로 로드 밸런싱 같은 역할을 한다.

- Coordinating node로 지정하기 위한 설정은 다음과 같다.

node.master: false 
node.data: false 
node.ingest: false 
node.ml: false 
cluster.remote.connect: false

 

Node Data Path Setting

path.data

- 모든 데이터와 master-eligible 노드는 샤드 그리고 인덱스, 클러스터 메타데이터가 저장되어 있는 데이터 디렉토리를 접근한다.

- path.data는 기본적으로 $ES_HOME/data로 지정되어 있지만 elasticsearch.yml을 통해서 바꿀 수 있다.

댓글()

Elasticsearch version conflict 에러

배치를 이용해서 Elasticsearch에 데이터를 삽입하던 중 version conflict라는 오류가 자주 발생했다. 처음에는 Elasticsearch 버전이 동일한데 왜? 오류가 나는지 몰랐다.

그래서 검색해보니 인덱스안에 document에는 각자 관리하는 version이 존재한다. 이 version은 document가 수정될 때 하나씩 올라가게 되는데 version이 10인 상태에 document에 여러 서버 모듈에서 해당 document에 업데이트를 하려고 하니 문제가 발생하였다.

그 이유는 version 10인 상태에서 작업에 들어간 두 모듈은 한 모듈이 먼저 11로 업데이트를 시키고 다음 모듈이 작업을 진행하려고 할 때 자기가 알고 있던 마지막 version인 10이 아니라 11로 바껴있는것을 보고 에러를 뱉어내는것이다. 이렇게 까지 세심하게 챙겨줄지 몰랐다. 알면 알수록 elasticsearch라는 db는 정말 매력적이다.

PUT wedul_index 
{
  "mappings": {
      "_doc": {
        "dynamic": "false",
        "properties": {
          "name": {
            "type": "text"
          }
        }
      }
  }
}

위와 같이 인덱스가 있고 document 하나가 들어있다. 여기에 age라는 값과 gender를 집어넣어보자. 이를 동시에 호출해보자.

document

그럼 document 하나에 필드를 동시에 업데이트하는 update.sh라는 스크립트를 만들어서 실행시켜보자.

curl -X POST "localhost:9200/wedul_index/_update_by_query" -H 'Content-Type: application/json' -d' { "script": { "source": "ctx._source[\u0027gender\u0027] = \u0027M\u0027"}, "query": { "match": { "name": "위들" } } } ‘
curl -X POST "localhost:9200/wedul_index/_update_by_query" -H 'Content-Type: application/json' -d' { "script": { "source": "ctx._source.age = 10", "lang": "painless" }, "query": { "match": { "name": "위들" } } } ‘

그럼 위에 설명했던 것 처럼 버전이 먼저 변경이 되면서 다음과 같은 에러를 뱉어낸다.

[{"index":"wedul_index","type":"_doc","id":"3MSd5WsB_jV9Cf9TkYLV","cause":{"type":"version_conflict_engine_exception","reason":"[_doc][3MSd5WsB_jV9Cf9TkYLV]: version conflict, current version [3] is different than the one provided [2]","index_uuid":"sJI8sBnrTP-OW8OG8YBqWA","shard":"3","index":"wedul_index"},"status":409}]

 

이를 해결하기 위해서는 retry_on_conflict 옵션을 함꼐 부여할 수 있는데 이 옵션은 version conflict이 발생했을 때, 업데이트 재시도를 몇회 할건지 지정하는 옵션이다.

좀 더 자세한 사항은 아래 elasticsearch 메뉴얼을 보면 자세히 나와있다.

참조
https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update-by-query.html
https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html

댓글()

Elasticsearch reindex시 alias를 사용하여 무중단으로 진행하기 & big index 리인덱싱 시 비동기 처리 방법

Elasticsearch reindex를 진행할 때, 단순하게 새로운 인덱스를 만들고 reindex api를 진행하고 기존 인덱스를 지우고 새로 만들어서 다시 reindex를 해줬다. (이전글: https://wedul.site/611?category=680504)

하지만 그것은 해당 인덱스의 document의 수가 적어서 금방 진행이 되었었고 만약 document수가 10만가지만 넘어도 생각보다 오래걸려서 서비스의 흐름이 끊어지게 된다는걸 인지하지 못했다. 같은 회사 동료분께서 해당 부분에 대해서 말씀해주셨고, 그 분이 가이드 해주신대로 진행해서 reindex를 무중단하게 진행하는 방법을 찾아봤다.

 

Alias를 이용하여 reindex하기


기존 index wedul의 매핑구조이다.

PUT wedul 
{
  "mappings": {
    "dynamic": false,
    "properties": {
      "name": {
        "type": "text"
      }
    }
  }
}

해당 인덱스의 데이터는 현재 다음과 같이 들어있는 것을 볼 수 있다. 여기서 age는 매핑이 안되어있어서 검색에 잡을 수 없기에 이를 reindex를 통해 매핑 정보를 업데이트해주자.

wedul 인덱스에 들어있는 데이터(왼), age로 검색이 안됨 (우)

그럼 reindex를 위해 새로운 인덱스 wedul_v1을 만들어보자.

reindex를 진행할 새로운 index, wedul_v1

그리고 wedul_v1으로 reindex를 실행해준다. 이때 주의사항이 있는데 document양이 10만 이상이 넘어가게 되면 작업이 오래걸리기에 kibana에서 504 gateway timeout이 발생하고 작업이 중단된다. 그래서 해당 작업을 비동기로 실행시키는 옵션인 wait_for_completion=false를 함께 설정해주고 진행해야한다.

POST _reindex?wait_for_completion=false
{
  "source": {
    "index": "wedul"
  },
  "dest": {
    "index": "wedul_v1"
  }
}

그럼 위에 이미지처럼 task 프로세스 번호가 나오고 이 프로세스에 시작시간 상태 취소 가능여부 등등을 GET _task 명령어를 통해 볼 수 있다. 여기서 프로세스가 종료되면 reindex가 다 된것이다.

그 다음 wedul_v1에 wedul이라는 alias를 지정해줘야한다. 

POST _aliases
{
  "actions": [
    {
      "add": {
        "index": "wedul_v1",
        "alias": "wedul"
      }
    }
  ]
}

alias를 지정하기 전에 기존 인덱스 wedul을 지워줘야한다. DELETE wedul 명령어를 날려서 기존 인덱스를 지우고 위의 alias 명령어를 실행시킨다. 

그럼 정상적으로 alias를 통해 무중단 reindex를 실행되었다. 정상적으로 실행 되었는지 age에 대한 query를 날려보자.

ㅋㅋ 정상적으로 실행되었다.

앞으로 이런 방식으로 진행해야겠다.

 

출처 : https://discuss.elastic.co/t/reindex-big-index/83047

 

Reindex big index

I would like to reindex a very big index. When I run reindex API with elasticsearchjs client I will receive the requestTimeout error, or Gateway timeout error. It's ok because the reindex process is still running in Elastic server. However, what I want to

discuss.elastic.co

https://www.elastic.co/kr/blog/changing-mapping-with-zero-downtime

댓글()

Elasticsearch nori 형태소 분석기에서 readingform filter를 이용해서 한자 내용을 한글로 변환하기

Elasticsearch filter에서 한자로 검색했을 때 일치하는 한글 결과로 tokenizing하게 해주는 filter가 있다. 해당 filter는 nori-readingform이다. 적용 방법은 기존에 synonmys나 speech필터 적용과 동일하다.

 

인덱스 생성


위에서 부터 사용했던 인덱스에 nori_readingform 필터를 추가해서 생성만 해주면 된다.

PUT wedul_anaylyzer
{
  "settings": {
    "index" : {
      "analysis" : {
        "tokenizer": {
          "nori_user_dict": {
            "type": "nori_tokenizer",
            "decompound_mode": "none",
            "user_dictionary": "dic/nori_userdict_ko.txt"
          }
        },
        "analyzer" : {
          "custom_analyze" : {
            "type": "custom",
            "tokenizer" : "nori_user_dict",
            "filter": [
              "my_posfilter",
              "nori_readingform"
            ]
          }
        },
        "filter": {
          "my_posfilter": {
            "type": "nori_part_of_speech",
            "stoptags": [
              "NP", "UNKNOWN"
            ]
          }
        }
      }
    }
  }
}

이렇게 만든 인덱스를 이용해서 한자를 이용해서 한글 내용을 뽑아내보자

결과


행복이라는 한자를 입력하여 검색해보자. 필터가 정상적으로 적용된다면 행복이라는 내용을 가진 결과가 나올것이다

GET wedul_analyzer/_analyze
{
"analyzer": "custom_analyze",
"text": "幸福 사랑"
}

결과는 정상적으로 행복 그리고 사랑이라는 단어로 추출되었다. nori를 공부하면서 좋은 기본 필터 많은걸 알게 되서 좋다.

댓글()

Elasticsearch 특정 형태소 종류를 제외하여 검색하는 필터 nori_part_of_speech 적용

Elasticsearch를 사용하여 analyze를 사용하다가 조사, 형용사 등등을 제외하고 형태소 토크나이즈가 되어야 했다. 그래서 정식 문서를 찾아보더니 nori_part_of_speech라는 필터가 있었다.

우선 저번 시간에 만들었던 wedul_analyzer 인덱스를 이용해서 토크나이즈를 해보자.

{
  "tokens": [
    {
      "token": "바보",
      "start_offset": 0,
      "end_offset": 2,
      "type": "word",
      "position": 0
    },
    {
      "token": "위들",
      "start_offset": 3,
      "end_offset": 5,
      "type": "word",
      "position": 1
    },
    {
      "token": "이",
      "start_offset": 5,
      "end_offset": 6,
      "type": "word",
      "position": 2
    },
    {
      "token": "집에",
      "start_offset": 7,
      "end_offset": 9,
      "type": "word",
      "position": 3
    },
    {
      "token": "서",
      "start_offset": 9,
      "end_offset": 10,
      "type": "word",
      "position": 4
    },
    {
      "token": "나",
      "start_offset": 11,
      "end_offset": 12,
      "type": "word",
      "position": 5
    },
    {
      "token": "왔다",
      "start_offset": 12,
      "end_offset": 14,
      "type": "word",
      "position": 6
    }
  ]
}

여기서 '나'와 '왔다'를 없애고 토크나이즈 결과가 나왔으면 좋겠다.

그럼 '나'와 '왔다'의 형태소가 어떻게 되는지 우선 알아보자. analyzer api에 explain: true 옵션을 부여하면 해당 토크나이즈에 분리된 형태소들의 정보가 나온다.

GET _analyze
{
  "analyzer": "nori",
  "explain": true, 
  "text": "바보 위들이 집에서 나왔다"
}

'나'와 '왔다'는 NP와 UNKNOWN이다.  이 두개를 nori_part_of_speech필터를 이용해서 제거해보자.

 {
          "token": "나",
          "start_offset": 11,
          "end_offset": 12,
          "type": "word",
          "position": 6,
          "bytes": "[eb 82 98]",
          "leftPOS": "NP(Pronoun)",
          "morphemes": null,
          "posType": "MORPHEME",
          "positionLength": 1,
          "reading": null,
          "rightPOS": "NP(Pronoun)",
          "termFrequency": 1
        },
        {
          "token": "왔다",
          "start_offset": 12,
          "end_offset": 14,
          "type": "word",
          "position": 7,
          "bytes": "[ec 99 94 eb 8b a4]",
          "leftPOS": "UNKNOWN(Unknown)",
          "morphemes": null,
          "posType": "MORPHEME",
          "positionLength": 1,
          "reading": null,
          "rightPOS": "UNKNOWN(Unknown)",
          "termFrequency": 1
        }

custom analyzer를 만들면서 nori_part_of_speech 필터를 추가해주면된다. 이 필터에서 stoptags 배열에 제거하고 싶은 형태소 요형을 추가하면 해당 형태소를 제거한 결과만 출력된다.

PUT wedul_anaylyzer
{
  "settings": {
    "index" : {
      "analysis" : {
        "tokenizer": {
          "nori_user_dict": {
            "type": "nori_tokenizer",
            "decompound_mode": "none",
            "user_dictionary": "dic/nori_userdict_ko.txt"
          }
        },
        "analyzer" : {
          "custom_analyze" : {
            "type": "custom",
            "tokenizer" : "nori_user_dict",
            "filter": [
              "my_posfilter"
            ]
          }
        },
        "filter": {
          "my_posfilter": {
            "type": "nori_part_of_speech",
            "stoptags": [
              "NP", "UNKNOWN"
            ]
          }
        }
      }
    }
  }
}

이렇게 만든 analyze를 이용해서 다시한번 확인해보자. 

아래 결과 처럼 '나'와 '왔다' 두개의 형태소가 사라진 것을 확인할 수 있다.

{
  "tokens": [
    {
      "token": "바보",
      "start_offset": 0,
      "end_offset": 2,
      "type": "word",
      "position": 0
    },
    {
      "token": "위들",
      "start_offset": 3,
      "end_offset": 5,
      "type": "word",
      "position": 1
    },
    {
      "token": "이",
      "start_offset": 5,
      "end_offset": 6,
      "type": "word",
      "position": 2
    },
    {
      "token": "집에",
      "start_offset": 7,
      "end_offset": 9,
      "type": "word",
      "position": 3
    },
    {
      "token": "서",
      "start_offset": 9,
      "end_offset": 10,
      "type": "word",
      "position": 4
    }
  ]
}

 

기본적으로 stoptags를 적용하지 않으면 10몇가지의 형태소 종류가 기본으로 배제된다.

NP, VPC등 형태소들에 대한 용어는 하단 사이트에 잘 정리되어 있다.

https://coding-start.tistory.com/167
http://kkma.snu.ac.kr/documents/?doc=postag

 

꼬꼬마, 한글 형태소 분석기 (Kind Korean Morpheme Analyzer, KKMA)

꼬꼬마 한국어 형태소 분석기 한글 형태소 품사 (Part Of Speech, POS) 태그표 한글 형태소의 품사를 '체언, 용언, 관형사, 부사, 감탄사, 조사, 어미, 접사, 어근, 부호, 한글 이외'와 같이 나누고 각 세부 품사를 구분한다. 대분류 세종 품사 태그 심광섭 품사 태그 KKMA 단일 태그 V 1.0 태그 설명 Class 설명 묶음1 묶음2 태그 설명 확률태그 저장사전 체언 NNG 일반 명사 NN 명사 N NN NNG 보통 명사 NNA no

kkma.snu.ac.kr

 

출처
https://www.elastic.co/guide/en/elasticsearch/plugins/6.4/analysis-nori-speech.html

댓글()
  1. Favicon of https://bodol-engineer.tistory.com BlogIcon 보현94 2020.04.07 14:53 신고 댓글주소  수정/삭제  댓글쓰기

    안녕하세요, Elasticsearch로 개발을 하고 있는 개발자입니다.
    Nori 관련해서 궁금해서 여쭤보려고 합니다.
    현재 제가 user_dict 안에 아래와 같이 넣고 인덱싱 작업을 진행하였습니다.
    - 칼맞은삼겹살, 칼, 맞은, 삼겹살

    decompound_mode는 mixed로 하였으며 복합명사로 만들었습니다. 또한 token 결과는 위의 네개로 분리되어 나오는걸 확인하였습니다.

    다만 5백만개의 데이터에 대해 search API를 사용하여 '칼맞은삼겹살' 검색 시 Exact매칭과 전방매칭을
    제외한 결과는 나오지 않고 있습니다. 제가 원하는 결과는 위 Exact매칭과 전방매칭이 노출 된 이후에 token에 '칼', '삼겹살'이 포함되어 있으므로 예를 들어 '칼먹은삼겹살', '칼삼겹살' 이러한 것도 표출이 되어야 한다 생각하는데 이 부분을 표출하기 위해서는 어떻게 처리해야 될 지 궁금해서 답글 남깁니다.