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에 '칼', '삼겹살'이 포함되어 있으므로 예를 들어 '칼먹은삼겹살', '칼삼겹살' 이러한 것도 표출이 되어야 한다 생각하는데 이 부분을 표출하기 위해서는 어떻게 처리해야 될 지 궁금해서 답글 남깁니다.

Elasticsearch에서 Dictionary 변경 시 analyzer와 인덱싱된 Document 갱신 방법

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 댓글주소  수정/삭제  댓글쓰기

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