Elasticsearch 대용량 데이터 조회 시 deep pagination 문제 해결

문제 상황

로그 분석 대시보드를 개발하던 중 특정 기간의 모든 로그를 export하는 기능에서 에러가 발생했다.

{
  "error": {
    "type": "query_phase_execution_exception",
    "reason": "Result window is too large, from + size must be less than or equal to: [10000]"
  }
}

Elasticsearch는 기본적으로 from + size가 10,000을 초과하는 deep pagination을 허용하지 않는다. 메모리 문제 때문이다.

시도한 방법들

1. max_result_window 증가

PUT /logs/_settings
{
  "index.max_result_window": 50000
}

임시방편이었다. 데이터가 더 늘어나면 같은 문제가 반복될 것이 뻔했다.

2. Scroll API

const response = await client.search({
  index: 'logs',
  scroll: '1m',
  body: {
    size: 1000,
    query: { match_all: {} }
  }
});

Scroll API는 실시간 검색에는 부적합하고, 컨텍스트 유지를 위한 리소스 소모가 있었다.

최종 해결: search_after

search_after를 사용하면 커서 기반으로 안전하게 페이지네이션할 수 있다.

const getAllLogs = async () => {
  const allResults = [];
  let searchAfter = null;

  while (true) {
    const body = {
      size: 1000,
      sort: [{ timestamp: 'asc' }, { _id: 'asc' }],
      query: { range: { timestamp: { gte: startDate, lte: endDate } } }
    };

    if (searchAfter) {
      body.search_after = searchAfter;
    }

    const response = await client.search({ index: 'logs', body });
    const hits = response.body.hits.hits;

    if (hits.length === 0) break;

    allResults.push(...hits.map(hit => hit._source));
    searchAfter = hits[hits.length - 1].sort;
  }

  return allResults;
};

주의사항

  • sort 필드에는 unique한 값이 포함되어야 한다. timestamp만으로는 부족할 수 있어 _id를 추가했다.
  • 인덱스 데이터가 실시간으로 변경되면 일부 문서를 놓치거나 중복 조회할 수 있다.
  • PIT(Point in Time)를 함께 사용하면 더 안정적이지만, 현재 ES 버전(6.8)에서는 지원하지 않는다.

결과

100만 건 이상의 로그 데이터를 안정적으로 조회할 수 있게 되었다. 메모리 사용량도 일정하게 유지되어 서버 부하가 줄었다.