본문 바로가기
제품/ELK

[ES]스크립팅: Painless

by 헬로웬디 2024. 7. 22.

개 요

Elasticsearch의 기본 스크립팅 언어는 Painless입니다. Elasticsearch는 대규모 데이터 세트에서 실시간 검색 및 분석을 지원하는 플랫폼으로, 이를 위해 안정적이고 효율적인 스크립팅 언어가 필요한데, Painless는 이러한 요구 사항을 충족시키기 위해 특별히 설계되었습니다. 검색, 정렬, 집계 등 다양한 용도로 사용되어 지고 있음을 예제를 통해 보여드릴께요.

 

우선  샘플 데이터 인덱싱을 먼저 합니다.

POST _bulk
{ "index" : { "_index" : "member", "_id" : "1" } }
{ "name" : "sally", "salary": 4000, "job": "Dancer" }
{ "index" : { "_index" : "member", "_id" : "2" } }
{ "name" : "john", "salary": 6000, "job": "Engineer" }
{ "index" : { "_index" : "member", "_id" : "3" } }
{ "name" : "alice", "salary": 5500, "job": "Teacher" }
{ "index" : { "_index" : "member", "_id" : "4" } }
{ "name" : "mike", "salary": 4800, "job": "Developer" }
{ "index" : { "_index" : "member", "_id" : "5" } }
{ "name" : "emily", "salary": 5200, "job": "Nurse" }
{ "index" : { "_index" : "member", "_id" : "6" } }
{ "name" : "david", "salary": 5100, "job": "Architect" }

 

 

painless 스크립트

물가도 올랐으니, 올해는 salary가 2배씩 올랐으면하는 바램을 스크립트로 나타내면 다음과 같습니다.

GET member/_search
{
  "script_fields": {
    "my_doubled_salary": {
      "script": { 
        "source": "doc['salary'].value * 2"
      }
    }
  }
}

 

만약 2배가아니라 3배로 값을 변경해야 할 경우, 위 스크립트는 다시 컴파일되어야 합니다. 스크립트를 수정하지 않고도 변수를 변경할 수 있도록 파라미터를 사용하면 Elasticsearch는 새로운 값을 적용할 때 다시 컴파일하지 않아도 됩니다. 이렇게 하면 성능을 최적화하고 실행 시간을 절약할 수 있습니다.

 

파라미터 이용하기

GET member/_search
{
  "script_fields": {
    "my_doubled_salary": {
      "script": { 
        "source": "doc['salary'].value * params['multiplier']", 
        "params": {
          "multiplier": 2
        }
      }
    }
  }
}
 
 

스크립트 따로 저장하기

스크립트를 따로 저장하고 필요할 때마다 실행하면 더 편리하겠죠. 

POST _scripts/my_doubled_salary
{
  "script": { 
    "lang": "painless", 
    "source": "doc['salary'].value * params['multiplier']"
  }
}
스크립트가 잘 저장되었나 확인해 봅니다. 
GET _scripts/my_doubled_salary

 

다음이 반환됩니다. 스크립트가 잘 저장되었는지 알 수 있습니다. 

 

저장된 스크립트를 호출하는 방법은 다음과 같습니다. 스크립트 대신 스크립트 _id를 적어주면 됩니다.

GET member/_search
{
  "script_fields": {
    "my_doubled_salary": {
      "script": { 
        "id": "my_doubled_salary", 
        "params": {
          "multiplier": 2
        }
      }
    }
  }
}

 

스크립트 삭제

저장된 스크립트는 삭제하려면 다음처럼 하세요.

DELETE _scripts/my_doubled_salary

 

 

스크립트로 도큐먼트 업데이트하기

여기서 우리는 ctx라는 특별한 변수를 보게 됩니다. ctx는 업데이트 쿼리의 경우 사용할 수 있는 특수 변수로, ctx가 현재 문서를 보유합니다. 그럼 모든 멤버의 월급을 10배로 올립니다.

POST member/_update_by_query
{
  "script": {
    "source": "ctx._source.salary *= 100",
    "lang": "painless"
  }
}

 

GET member/_search

 

예제

  • def

def 타입 변수를 선언하면 어떤 타입의 값이든 할당할 수 있습니다.

    def v1 = ctx.s1?.src?.process?.parent?.cmdline;
    def v2 = ctx.s1?.tgt?.process?.cmdline;

 

  • instanceof 
    instanceof 연산자는 변수/필드 타입을 지정된 참조 타입과 비교하는데, 참조 타입과 동일하면 true를, 그렇지 않으면 false를 반환합니다. 
# v1의 타입이 String이며 true, 아니면 false를 반환
# v1의 타입이 String이고 null이 아니면 

if (v1 instanceof String && v1 != null) {
	v1 = trimQuotes(params.double_quote, v1);
    ctx.s1.src.process.parent.put("cmdline", v1);
}

if (v2 instanceof String && v2 != null) {
    v2 = trimQuotes(params.double_quote, v2);
    ctx.s1.tgt.process.put("cmdline", v2);
  }

 

  • 함수
     

    함수는 특정 작업을 수행하는 코드 블록으로, 이름을 부여하여 정의합니다. 함수는 반환 타입 값을 지정하는데, 반환 타입이 void인 경우에는 값이 반환되지 않습니다. 문자열에서 doubleQuote를 제거하고 문자만 추출하는 함수를 정의해봅시다.

# 변수 v에 담긴 문자열이 doubleQuote로 감싸져 있다면,
# 문자열 v에서 두 번째 문자부터 마지막 전 문자까지의 부분 문자열을 추출합니다.

String trimQuotes(def doubleQuote, def v) {
	if (v.startsWith(doubleQuote) && v.endsWith(doubleQuote)) {
    	v = v.substring(1, v.length() - 1);
    }
    return v;
}

 

 

Painless 스크립트의 시작 부분에 함수들을 선언할 수 있습니다. 

String trimQuotes(def doubleQuote, def v) {
	if (v.startsWith(doubleQuote) && v.endsWith(doubleQuote)) {
    	v = v.substring(1, v.length() - 1);
    }
    return v;
}

def v1 = ctx.s1?.src?.process?.parent?.cmdline;
def v2 = ctx.s1?.tgt?.process?.cmdline;
if (v1 instanceof String && v1 != null) {
    v1 = trimQuotes(params.double_quote, v1);
    ctx.s1.src.process.parent.put("cmdline", v1);
}
if (v2 instanceof String && v2 != null) {
    v2 = trimQuotes(params.double_quote, v2);
    ctx.s1.tgt.process.put("cmdline", v2);
}

 

ingest pipeline에서 문자열에서 "  을 제거하는 script 프로세서를 다음처럼 작성할 수 있습니다. 

  {
    "script": {
      "description": "Script to remove quotes from begining and end from a field.",
      "tag": "script_to_remove_quotes_from_begining_and_end",
      "source": """String trimQuotes(def doubleQuote, def v) {
        if (v.startsWith(doubleQuote) && v.endsWith(doubleQuote)) {
          v = v.substring(1, v.length() - 1);
        }
      return v;
      }
      def v1 = ctx.s1?.src?.process?.parent?.cmdline;
      def v2 = ctx.s1?.tgt?.process?.cmdline;
      if (v1 instanceof String && v1 != null) {
        v1 = trimQuotes(params.double_quote, v1);
        ctx.s1.src.process.parent.put("cmdline", v1);
      }
      if (v2 instanceof String && v2 != null) {
        v2 = trimQuotes(params.double_quote, v2);
        ctx.s1.tgt.process.put("cmdline", v2);
      }
      """,
      "lang": "painless",
      "params": {
        "double_quote": "\""
      },
      "on_failure": [
        {
          "append": {
            "field": "error.message",
            "value": "Processor {{{_ingest.on_failure_processor_type}}} with tag {{{_ingest.on_failure_processor_tag}}} in pipeline {{{_ingest.pipeline}}} failed with message: {{{_ingest.on_failure_message}}}"
          }
        }
      ]
    }
  }