oguri's garage

OpenSearch nested 쿼리와 집계 방식의 차이점 본문

개발하다/OpenSearch

OpenSearch nested 쿼리와 집계 방식의 차이점

oguri 2025. 10. 31. 22:51

문제 상황

점수 집계 기능을 개발하던 중, 흥미로운 질문이 생겼다.
하나의 문서에 여러 엔티티가 연관되어 있을 때, 검색 결과에서 이 문서가 몇 번 카운트될까?
예를 들어, 2개의 엔티티가 모두 검색 조건에 해당하는 문서가 있다면, 이 문서는 1번 카운트될까 2번 카운트될까?

 

 

 



OpenSearch의 nested 쿼리 동작 원리

결론부터 말하면, 1번만 카운트된다.
OpenSearch는 문서(document) 단위로 검색하기 때문이다.

nested 쿼리의 동작 방식을 이해하기 위해 간단한 예시를 살펴보자.

예시 데이터

{
  "_id": "doc123",
  "items": [
    {"name": "항목A"},
    {"name": "항목B"}
  ],
  "date": "2025-05-15"
}



검색 쿼리 구조

{
  "bool": {
    "should": [
      {
        "nested": {
          "path": "items",
          "query": {
            "term": {"items.name.keyword": "항목A"}
          }
        }
      },
      {
        "nested": {
          "path": "items",
          "query": {
            "term": {"items.name.keyword": "항목B"}
          }
        }
      }
    ],
    "minimum_should_match": 1
  }
}

 

이 쿼리는 "항목A 또는 항목B가 포함된 문서"를 찾는다.
중요한 점은 같은 문서는 조건을 여러 번 만족하더라도 결과에 한 번만 포함된다는 것이다.
마치 집합(Set)처럼 동작한다고 생각하면 쉽다.

 

 

 



집계 로직의 두 가지 접근

검색 결과를 집계하는 방식은 비즈니스 요구사항에 따라 달라질 수 있다.

1. 문서 단위 집계 (현재 방식)

Map<String, Integer> aggregationMap = new HashMap<>();

for (SearchHit hit : searchResponse.getHits()) {
    Document doc = hit.getSource();
    if (doc != null && doc.getDate() != null) {
        String dateKey = doc.getDate().format(formatter);
        aggregationMap.put(dateKey, 
            aggregationMap.getOrDefault(dateKey, 0) + 1);
    }
}

 

이 방식은 문서 자체를 카운트한다. 위 예시에서는 "2025-05" 에 1개가 집계된다.



2. nested 필드 단위 집계

만약 항목별로 따로 카운트하고 싶다면 다음과 같이 구현할 수 있다.

Map<String, Integer> aggregationMap = new HashMap<>();

for (SearchHit hit : searchResponse.getHits()) {
    Document doc = hit.getSource();
    if (doc != null && doc.getDate() != null) {
        String dateKey = doc.getDate().format(formatter);

        // nested 배열을 순회하며 각각 카운트
        for (Item item : doc.getItems()) {
            if (isMatchingSearchCondition(item)) {
                String key = dateKey + "|" + item.getName();
                aggregationMap.put(key, 
                    aggregationMap.getOrDefault(key, 0) + 1);
            }
        }
    }
}

 

이 경우 "2025-05|항목A" 와 "2025-05|항목B" 각각 1개씩, 총 2개가 집계된다.


 



실제 코드에서의 패턴 비교

같은 시스템 내에서도 데이터 특성에 따라 다른 집계 방식을 사용하고 있었다.

패턴 A: nested 필드 순회 집계

// 하나의 이벤트가 여러 태그를 가지는 경우
for (SearchHit hit : searchResponse.getHits()) {
    Event event = hit.getSource();

    if (event.getTags() != null && !event.getTags().isEmpty()) {
        for (Tag tag : event.getTags()) {
            String tagName = tag.getName() != null ? tag.getName() : "Unknown";
            String key = dateKey + "|" + tagName;
            aggregationMap.put(key, 
                aggregationMap.getOrDefault(key, 0) + 1);
        }
    }
}

 

1개 문서에 3개 태그가 있으면 → 3번 카운트



패턴 B: 문서 단위 집계

// 문서 자체를 카운트하는 경우
for (SearchHit hit : searchResponse.getHits()) {
    Document doc = hit.getSource();
    if (doc != null && doc.getDate() != null) {
        String dateKey = doc.getDate().format(formatter);
        aggregationMap.put(dateKey, 
            aggregationMap.getOrDefault(dateKey, 0) + 1);
    }
}

 

1개 문서에 2개 항목이 연관되어 있어도 → 1번 카운트

 

 

 



핵심 정리

단계 동작 결과
OpenSearch 검색 nested 조건에 여러 항목이 매칭되어도 문서는 1개만 반환됨
집계 방식 선택 문서 기준 vs nested 필드 기준 비즈니스 요구사항에 따라 결정



중요한 것은 OpenSearch의 검색 동작애플리케이션 레벨의 집계 로직을 구분해서 이해하는 것이다.
검색은 항상 문서 단위로 이루어지지만, 검색 결과를 어떻게 집계할지는 별개의 문제다.

이번 경험을 통해 같은 데이터 구조라도 집계 목적에 따라 완전히 다른 로직이 필요할 수 있다는 것을 배웠다.
특히 nested 필드를 다룰 때는 "무엇을 카운트의 단위로 볼 것인가"를 명확히 정의하는 것이 중요하다.