스크립트 메트릭 집계

스크립트 메트릭 집계 (Scripted metric aggregation)

scripted_metric 집계는 지정된 스크립트로부터 계산된 메트릭을 반환하는 다중 값(multi-value) 메트릭 집계예요. 스크립트는 init, map, combine, reduce의 네 가지 단계로 구성되며, 각 집계에 의해 순서대로 실행되어 문서들의 결과를 결합할 수 있게 해줘요.

네 개의 스크립트 모두 사용자가 정의하는 변경 가능한(mutable) 객체인 state를 공유해요. state는 init, map, combine 단계 동안 각 샤드에 국한되어 있어요. 결과는 reduce 단계를 위해 states 배열로 전달돼요. 따라서 각 샤드의 state는 reduce 단계에서 샤드들이 결합되기 전까지 독립적이에요.

출처: 문서

본문

파라미터

scripted_metric 집계는 다음과 같은 파라미터를 받아요.

파라미터 데이터 타입 필수/선택 설명
init_script String 선택 문서가 처리되기 전에 샤드마다 한 번씩 실행되는 스크립트예요. 초기 상태를 설정하는 데 사용돼요(예: state 객체에서 카운터나 목록 초기화). 제공하지 않으면 각 샤드에서 state가 빈 객체로 시작돼요.
map_script String 필수 집계가 수집한 각 문서에 대해 실행되는 스크립트예요. 이 스크립트는 문서의 데이터를 기반으로 state를 갱신해요. 예를 들어 필드 값을 확인한 다음 카운터를 증가시키거나 state에 누적 합계를 계산할 수 있어요.
combine_script String 필수 해당 샤드의 모든 문서가 map_script로 처리된 후 샤드마다 한 번씩 실행되는 스크립트예요. 이 스크립트는 샤드의 state를 조정 노드(coordinating node)로 보낼 단일 결과로 집계해요. 이 스크립트는 한 샤드의 계산을 마무리하는 데 사용돼요(예: state에 저장된 카운터나 합계를 더하기). 이 스크립트는 샤드에 대한 통합된 값 또는 구조를 반환해야 해요.
reduce_script String 필수 모든 샤드로부터 결합된 결과를 받은 후 조정 노드에서 한 번 실행되는 스크립트예요. 이 스크립트는 combine_script의 각 샤드 출력을 담은 배열인 특별한 변수 states를 받아요. reduce_script는 states를 순회하며 최종 집계 출력을 생성해요(예: 샤드 합계 더하기 또는 카운트 맵 병합). reduce_script가 반환한 값이 집계 결과에 보고되는 값이에요.
params Object 선택 reduce_script를 제외한 모든 스크립트에서 접근할 수 있는 사용자 정의 파라미터예요.

허용되는 반환 타입 (Allowed return types)

스크립트는 내부적으로 어떤 유효한 연산과 객체든 사용할 수 있어요. 그러나 state에 저장하거나 어떤 스크립트에서 반환하는 데이터는 반드시 허용된 타입 중 하나여야 해요. 중간 state를 노드 간에 전송해야 하기 때문에 이 제한이 존재해요. 허용되는 타입은 다음과 같아요:

  • 기본 타입(Primitive types): int, long, float, double, boolean
  • String
  • Map(키와 값이 허용된 타입만으로 구성되어야 함: 기본 타입, 문자열, 맵 또는 배열)
  • Array(허용된 타입만 포함: 기본 타입, 문자열, 맵 또는 배열)

state는 숫자, 문자열, 맵(객체) 또는 배열(목록) 또는 이들의 조합일 수 있어요. 예를 들어 맵을 사용해 여러 카운터를 누적하거나, 배열로 값을 수집하거나, 단일 숫자로 누적 합계를 유지할 수 있어요. 여러 메트릭을 반환해야 한다면 맵 또는 배열에 저장할 수 있어요. reduce_script에서 맵을 최종 값으로 반환하면 집계 결과에 객체가 포함돼요. 단일 숫자나 문자열을 반환하면 결과는 단일 값이에요.

스크립트에서 파라미터 사용하기 (Using parameters in scripts)

params 필드를 사용해 스크립트에 사용자 정의 파라미터를 선택적으로 전달할 수 있어요. 이것은 사용자 정의 객체이며, 그 내용물이 init_script, map_script, combine_script에서 사용할 수 있는 변수가 돼요. reduce_script는 직접 params를 받지 않아요. reduce 단계에서는 필요한 모든 데이터가 states 배열에 있어야 하기 때문이에요. reduce 단계에서 상수가 필요하면 각 샤드의 state의 일부로 포함하거나 저장된(stored) 스크립트를 사용할 수 있어요. 모든 파라미터는 전역 params 객체 안에 정의되어야 해요. 그래야 다른 스크립트 단계 간에 공유될 수 있어요. params를 지정하지 않으면 params 객체는 비어 있어요.

예를 들어, params에 임계값이나 필드 이름을 제공한 다음 스크립트에서 params.threshold 또는 params.field를 참조할 수 있어요:

"scripted_metric": {
  "params": {
    "threshold": 100,
    "field": "amount"
  },
  "init_script": "...",
  "map_script": "...",
  "combine_script": "...",
  "reduce_script": "..."
}

예제

다음 예제들은 scripted_metric을 사용하는 다양한 방법을 보여줘요.

거래에서 순이익 계산하기 (Calculating net profit from transactions)

다음 예제는 내장 집계로는 직접 지원되지 않는 사용자 지정 메트릭을 계산하기 위해 scripted_metric 집계를 사용하는 방법을 보여줘요. 데이터셋은 금융 거래를 나타내며, 각 문서는 판매(income) 또는 비용(expense)으로 분류되고 amount 필드를 포함해요. 목표는 모든 문서에 걸쳐 총 판매액에서 총 비용을 빼 전체 순이익을 계산하는 것이에요.

인덱스를 생성해요:

PUT transactions
{
  "mappings": {
    "properties": {
      "type":   { "type": "keyword" }, 
      "amount": { "type": "double" }
    }
  }
}

두 건의 판매(금액 80과 130)와 두 건의 비용(10과 30), 총 네 건의 거래를 인덱스에 넣어요:

PUT transactions/_bulk?refresh=true
{ "index": {} }
{ "type": "sale", "amount": 80 }
{ "index": {} }
{ "type": "cost", "amount": 10 }
{ "index": {} }
{ "type": "cost", "amount": 30 }
{ "index": {} }
{ "type": "sale", "amount": 130 }

이익을 계산하는 scripted_metric 집계가 있는 검색을 실행하려면 다음 스크립트들을 사용해요:

  • init_script는 각 샤드의 거래 값을 저장하는 데 사용할 빈 목록을 생성해요.
  • map_script는 type이 sale이면 state.transactions 목록에 각 문서의 amount를 양수로, cost이면 음수로 추가해요. map 단계가 끝나면 각 샤드에는 수익과 비용을 나타내는 state.transactions 목록이 있어요.
  • combine_script는 state.transactions 목록을 처리해 샤드에 대한 단일 shardProfit 값을 계산해요. shardProfit은 샤드의 출력으로 반환돼요.
  • reduce_script는 조정 노드에서 실행되며 각 샤드의 shardProfit 값을 보유한 states 배열을 받아요. null 항목을 확인하고 이 값들을 더해 전체 이익을 계산한 뒤 최종 결과를 반환해요.

다음 요청에는 설명한 모든 스크립트가 포함돼 있어요:

GET transactions/_search
{
  "size": 0, 
  "aggs": {
    "total_profit": {
      "scripted_metric": {
        "init_script": "state.transactions = []",
        "map_script": "state.transactions.add(doc['type'].value == 'sale' ? doc['amount'].value : -1 * doc['amount'].value)",
        "combine_script": "double shardProfit = 0; for (t in state.transactions) { shardProfit += t; } return shardProfit;",
        "reduce_script": "double totalProfit = 0; for (p in states) { if (p != null) { totalProfit += p; }} return totalProfit;"
      }
    }
  }
}

응답은 total_profit을 반환해요:

{
  ...
  "hits": {
    "total": {
      "value": 4,
      "relation": "eq"
    },
    "max_score": null,
    "hits": []
  },
  "aggregations": {
    "total_profit": {
      "value": 170
    }
  }
}

HTTP 응답 코드 분류하기 (Categorizing HTTP response codes)

다음 예제는 단일 집계 내에서 여러 값을 반환하는 scripted_metric 집계의 더 고급 사용법을 보여줘요. 데이터셋은 각각 HTTP 응답 코드를 포함하는 웹 서버 로그 항목으로 구성돼요. 목표는 응답을 성공 응답(2xx 상태 코드), 클라이언트 또는 서버 오류(4xx 또는 5xx 상태 코드), 기타 응답(1xx 또는 3xx 상태 코드)의 세 가지 범주로 분류하는 것이에요. 이 분류는 맵 기반 집계 상태 내에서 카운터를 유지하는 방식으로 구현돼요.

샘플 인덱스를 생성해요:

PUT logs
{
  "mappings": {
    "properties": {
      "response": { "type": "keyword" }
    }
  }
}

다양한 응답 코드를 가진 샘플 문서를 추가해요:

PUT logs/_bulk?refresh=true
{ "index": {} }
{ "response": "200" }
{ "index": {} }
{ "response": "201" }
{ "index": {} }
{ "response": "404" }
{ "index": {} }
{ "response": "500" }
{ "index": {} }
{ "response": "304" }

(각 샤드의) state는 error, success, other 세 개의 카운터를 가진 맵이에요.

범주를 세는 스크립트 메트릭 집계를 실행하려면 다음 스크립트들을 사용해요:

  • init_script는 error, success, other 카운터를 0으로 초기화해요.
  • map_script는 각 문서의 응답 코드를 검사하고 응답 코드에 따라 해당 카운터를 증가시켜요.
  • combine_script는 해당 샤드에 대한 state.responses 맵을 반환해요.
  • reduce_script는 모든 샤드의 맵 배열(states)을 병합해요. 즉 새 결합 맵을 만들고 각 샤드 맵의 error, success, other 개수를 더해요. 이 결합 맵이 최종 결과로 반환돼요.

다음 요청에는 설명한 모든 스크립트가 포함돼 있어요:

GET logs/_search
{
  "size": 0,
  "aggs": {
    "responses_by_type": {
      "scripted_metric": {
        "init_script": "state.responses = new HashMap(); state.responses.put('success', 0); state.responses.put('error', 0); state.responses.put('other', 0);",
        "map_script": """
          String code = doc['response'].value;
          if (code.startsWith("5") || code.startsWith("4")) {
            // 4xx or 5xx -> count as error
            state.responses.error += 1;
          } else if (code.startsWith("2")) {
            // 2xx -> count as success
            state.responses.success += 1;
          } else {
            // anything else (e.g., 1xx, 3xx, etc.) -> count as other
            state.responses.other += 1;
          }
        """,
        "combine_script": "return state.responses;",
        "reduce_script": """
          Map combined = new HashMap();
          combined.error = 0;
          combined.success = 0;
          combined.other = 0;
          for (state in states) {
            if (state != null) {
              combined.error += state.error;
              combined.success += state.success;
              combined.other += state.other;
            }
          }
          return combined;
        """
      }
    }
  }
}

응답은 value 객체에 세 개의 값을 반환하며, 스크립트 메트릭이 state에서 맵을 사용해 한 번에 여러 메트릭을 반환할 수 있음을 보여줘요:

{
  ...
  "hits": {
    "total": {
      "value": 5,
      "relation": "eq"
    },
    "max_score": null,
    "hits": []
  },
  "aggregations": {
    "responses_by_type": {
      "value": {
        "other": 1,
        "success": 2,
        "error": 2
      }
    }
  }
}

빈 버킷 관리 (문서가 없는 경우) (Managing empty buckets)

scripted_metric 집계를 버킷 집계(예: terms) 내의 하위 집계로 사용할 때, 특정 샤드에서 문서를 포함하지 않는 버킷을 고려하는 것이 중요해요. 이런 경우 해당 샤드는 집계 상태에 대해 null 값을 반환해요. 따라서 reduce 스크립트 단계에서 states 배열에 이러한 샤드에 해당하는 null 항목이 포함될 수 있어요. 안정적인 실행을 보장하려면 reduce_script가 null 값을 적절히 처리하도록 설계되어야 해요. 일반적인 방법은 각 state에 접근하거나 작업하기 전에 if (state != null) 같은 조건부 확인을 포함하는 것이에요. 이러한 확인을 구현하지 않으면 샤드 전체에서 빈 버킷을 처리할 때 런타임 오류가 발생할 수 있어요.

성능 고려 사항 (Performance considerations)

스크립트 메트릭은 모든 문서에 대해 사용자 지정 코드를 실행하므로 잠재적으로 큰 인메모리 상태를 누적할 수 있어, 내장 집계보다 느릴 수 있어요. 각 샤드의 중간 상태를 조정 노드로 보내기 위해 직렬화해야 해요. 따라서 상태가 매우 크면 많은 메모리와 네트워크 대역폭을 소비할 수 있어요. 검색을 효율적으로 유지하려면 스크립트를 최대한 가볍게 만들고 불필요한 데이터를 상태에 누적하지 마세요. 거래에서 순이익 계산하기(Calculating net profit from transactions)에서 보여준 것처럼 전송 전에 상태 데이터를 줄이기 위해 combine 단계를 사용하고, 최종 메트릭을 생성하는 데 실제로 필요한 값들만 수집하세요.

더 알아보기 (Learn more)