배치를 이용해서 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를 진행할 때, 단순하게 새로운 인덱스를 만들고 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 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를 사용하여 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

Elasticsearch에서 Dictionary를 사용하여 analyzer를 만들고 그를 사용해서 index에 Document를 인덱싱할 수 있다. 근데 Dictionary가 변경되면 analyzer를 변경하고 indexing된 document를 갱신하려면 어떻게 해야하는지 정리해보자.

Background 지식


Analyzer는 character filter, tokenizer, token filter 순서대로 적용한다. 기본적으로 anaylyzer는 indexing time과 search time에 적용된다. index time 분석 대상은 source data(원본 데이터)이고 search time 분석 대상은 query string이다. 그러므로 사전을 변경하는 것은 indexing, serching 두개 모두 영항을 준다.

사전 업데이트 방법


엘라스틱서치에서 analyzer는 index가 close/open될 때 사전을 읽는다. 그리고 일반적으로 로딩된 이후로는 다시 사전을 읽어 들이지 않는다. 그러므로 수정된 사전을 업데이트 하기 위해서는 dictionary file을 가지고 있는 node를 재시작하거나 index를 _close, _open해야한다.

예를들어 위메프라고 형태소를 나눴을 때, 위메프라는 명사를 알지 못해 다음과 같이 쪼개진다.

GET nori_sample/_analyze
{
"analyzer": "my_analyzer",
"text" : "위메프"
}

anaylze api 결과

그럼 명사라는걸 알려주기 위해서 사전에 위메프를 추가해보자.

그리고 다시 검색을 해보자.
하지만 결과는 처음과 같다. 위에 말한 것 처럼 반영해주기 위해서는 node를 재시작하거나 index를 닫았다가 열어야한다.

다시 검색한 검색결과는 똑같다.

그럼 index를 _close했다가 _open해보자.

POST nori_sample/_close
POST nori_sample/_open


그리고 결과를 다시 확인하면 잘 구분된걸 확인할 수 있다.

정상적으로 변경된 Dictionary가 반영된걸 볼 수있다.

하지만 이 방식으로 사전 업데이트는 이미 인덱싱된 document에는 적용되지는 않는다. 왜냐하면 document는 사전이 업데이트 되기전에 analyzer를 사용해서 인뎅싱 되기 때문이다. 그래서 사전이 업데이트 되었다고해서 사전이 적용되어서 검색결과가 변경되어 나오지는 않는다.

그럼 어떻게 변경된 사전정보를 이미 존재하는 indices에 적용할 수 있을까?

엘라스틱서치에서는 인덱스된 document가 업데이트 되었을 때, document는 제거되고 다시 생성된다. 이때 우리가 업데이트한 사전정보를 이용해서 document가 다시 인덱싱된다. 그렇기 때문에 update by query api를 사용하여 인덱스에 모든 정보를 업데이트해야한다.

update by query 사용방법은 다음과 같다.

update by query 사용방법

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

 

 

출처 : https://www.elastic.co/kr/blog/dictionary-update-behavior-for-elasticsearch-cjk-language-analyzers

  1. 최중한 2019.09.04 16:53

    안녕하세요 글 잘봤습니다:)
    궁금한 것이 있는데요, 이 작업을 할 때 리소스를 많이 써야 해서 검색 서비스에 영향을 줄 수도 있다고 하는데
    이걸 회피하는 방법은 어떤 것이 있는 지 궁금합니다.

    • Favicon of https://wedul.site BlogIcon 위들 wedul 2019.09.04 23:23 신고

      어느 동작에서 리소스를 많이 잡아먹어서 해결했으면 좋겠는지 알려주실수 있으신가요??

+ Recent posts