본문 바로가기
ElasticSearch

Spring Data Elasticsearch 를 활용한 Search After 구현

by Heesu.lee 2021. 3. 1.

Spring Data Elasticsearch (버전 4.1) 를 이용한 Pagination 구현 시 from, size 를 활용하여 원하는 Page 를 Size 개수만큼 얻을 수 있다.

Spring Data Elasticsearch 사용할 경우 PageRequest 를 사용하여 Pagination 을 구현할 수 있다.
@RequiredArgsConstructor
@Service
public class AccountSearchService {

    private final AccountRepository accountRepository;

	...

    public Flux<SearchHit<Account>> searchAllWithPagination(int offset, int size) {
        NativeSearchQuery query = new NativeSearchQueryBuilder()
                .withQuery(matchAllQuery())
                .withPageable(PageRequest.of(offset, size))
                .withSort(
                	new FieldSortBuilder("account_number")
                    	.order(SortOrder.ASC)
                )
                .build();
        return accountRepository.search(query);
    }
	
    ...
}

 

그렇지만, 조회 개수가 10000개를 넘아가는 순간 Elasticsearch 에서는 에러를 발생시킨다.

query_phase_execution_exception

See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting.cs

from 과 size 를 활용한 Pagination 은 분산된 Shard 부터 요청된 데이터를 메모리에 올려서 찾고 모아서 정렬 작업을 한 후 클라이언트가 요청한 Page 를 반환하기 때문에 10000 개가 넘는 데이터를 조회할 수 없도록 막고 있다. (index.max_result_window)

Avoid using from and size to page too deeply or request too many results at once. Search requests usually span multiple shards. Each shard must load its requested hits and the hits for any previous pages into memory. For deep pages or large sets of results, these operations can significantly increase memory and CPU usage, resulting in degraded performance or node failures.

이를 해결하기 위해 를 기본값인 10000개가 넘도록 설정하는 것도 하나의 방식일 수 있겠지만 이는 메모리 및 성능상 이슈를 야기시킬 수 있기 때문에 권장되지 않는 방식이다. (최대 50000개 까지 설정 가능)

 

이런 Deep Paging 필요한 경우 Elasticsearch 에서는 Scroll API 혹은 Search After 기능을 사용할 수 있다.

허나, Elasticsearch 버전 7부터 Scroll API 사용보단 Search After 사용을 권장하고 있다.

By default, you cannot use from and size to page through more than 10,000 hits. This limit is a safeguard set by the index.max_result_window index setting. If you need to page through more than 10,000 hits, use the search_after parameter instead.

Elasticsearch 에서 Search After 를 사용하기 위해선 반드시 Sort 기준이 필요하다.

Sort 필드

위 처럼 Sort 기준(필드)를 정의하여 요청하면 다음 아래와 같이 Document 들이 조회가 된다.

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1000,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [
      {
        "_index" : "account",
        "_type" : "_doc",
        "_id" : "0",
        "_score" : null,
        "_source" : {
          "account_number" : 0,
          "balance" : 16623,
          "firstname" : "Bradshaw",
          "lastname" : "Mckenzie",
          "age" : 29,
          "gender" : "F",
          "address" : "244 Columbus Place",
          "employer" : "Euron",
          "email" : "bradshawmckenzie@euron.com",
          "city" : "Hobucken",
          "state" : "CO"
        },
        "sort" : [
          0
        ]
      },
    
    ...
}

해당 조회된 Document 마다 Sort 필드를 가지게 되는데 Search After 는 해당 필드를 이용하여 다음 필드를 조회해 나가는 것이다. 

GET /account/_search
{
  "query": {
    "match_all": {}
  }, 
  "sort": [
    {
      "account_number": {
        "order": "asc"
      }
    }
  ],
  "search_after": [
    9 // 마지막 문서의 Sort 필드
  ]
}
중요 - 참고로 조회 요청 시 Sort 기준엔 반드시 유니크한 필드가 포함되어 있어야 한다.

Spring Data Elasticsearch 를 활용한 Search After 기능 활용하기

기존 Spring Data Elasticsearch 의 경우 (버전 4.1 or 4.0 기준) Search After 기능을 따로 사용할 수 있는 인터페이스가 없었다.

그렇지만, 최근 4.2.0-M4 버전에서 Search After 를 지원하기 위한 커밋이 반영된 것을 확인할 수 있었다.

(사수님의 도움으로 찾을 수 있었다. - 감사드립니다.)

github.com/spring-projects/spring-data-elasticsearch/pull/1691/commits/5120483d11a9d37bf833c89732e1cf87421d5d57

 

Add support for search_after. by sothawo · Pull Request #1691 · spring-projects/spring-data-elasticsearch

Closes #1143

github.com

아직 정식 출시된 버전이 아니지만, 해당 기능을 곧장 사용해야하기 때문에 해당 프리 버전을 사용하여 Search After 기능을 사용해볼 수 있도록 하겠습니다.

 

build.gradle

...

repositories {
	mavenCentral()
	maven {
		url "https://repo.spring.io/milestone/"
	}
}

...

dependencies {

	...

	// spring data
	implementation("org.springframework.data:spring-data-elasticsearch:4.2.0-M4")
	
    ...
}

반드시 milestone 에서 해당 버전 의존성을 가지기 위해 repositories 에 추가해주셔야 합니다.

 

Test Code for Search After

@SpringBootTest
class AccountSearchServiceTest {

    @Autowired ReactiveElasticsearchOperations operations;
    @Autowired AccountSearchService accountService;

    @DisplayName("Search After Test")
    @Test
    void searchAfterTest() {
        // given
        List<Account> entities = accountService.searchAll()
                .map(SearchHit::getContent)
                .collectList().block();

        Query query = Query.findAll();
        query.setPageable(PageRequest.of(0, 100));
        query.addSort(Sort.by(Sort.Direction.ASC, "account_number"));

        List<Object> searchAfter = null;
        List<Account> foundEntities = new ArrayList<>();

        // when
        int loop = 0;
        do {
            query.setSearchAfter(searchAfter);
            List<SearchHit<Account>> searchHits = 
            	operations.search(query, Account.class).collectList().block();

            assertThat(searchHits).isNotNull();
            
            if (searchHits.size() == 0) {
                break;
            }
            
            foundEntities.addAll(searchHits
            	.stream()
            	.map(SearchHit::getContent)
                .collect(Collectors.toList()));
                
            searchAfter = 
            	searchHits.get((searchHits.size() - 1)).getSortValues();

            if (++loop > 10) {
                fail("loop not terminating");
            }
        } while (true);

        // then
        assertThat(foundEntities).containsExactlyElementsOf(entities);
    }
}

여기서 눈 여겨볼 부분은 기존 Query 인터페이스에서 Search After 에 대한 지원이 없었지만,

이번 버전을 통해서 setSearchAfter 메서드를 통해 해당 기능을 사용할 수 있게 되었다.

 

아직 정식 출시가 되지 않아 사용하기 어려울 순 있겠지만, 조만간 해당 기능을 기존 Query 인터페이스를 통해 편하게 구현할 수 있다는 점에서 다음 버전 출시에 대한 기대감을 높일 수 있었다.

 

만약 현재 정식 출시된 버전에서 Search After 를 사용하고자 한다면 Client(RestHighLevelClient 등) 를 통해 SearchRequest 를 직접 생성하여 요청하는 방식을 사용해볼 수 있다.

(이는 다음 블로그를 참고해주시길 바랍니다. - 링크)

참고

  • ElasticSearch 공식 홈페이지 - 링크
 

Paginate search results | Elasticsearch Reference [7.11] | Elastic

The results that are returned from a scroll request reflect the state of the data stream or index at the time that the initial search request was made, like a snapshot in time. Subsequent changes to documents (index, update or delete) will only affect late

www.elastic.co

  • Spring Data Elasticsearch Github - 링크
 

Add support for search_after. by sothawo · Pull Request #1691 · spring-projects/spring-data-elasticsearch

Closes #1143

github.com

  • Wedul 님의 블로그 - 링크
 

RestHighLevelClient를 사용하여 search after 기능 구현하기

https://wedul.site/541에서 search after 기능을 사용해서 검색을 하는 이유를 알아봤었다. 그럼 spring boot에서 RestHighLevelClient를 이용해서 search after를 구현을 해보자. 1. Mapping 우선 index가 필요..

wedul.site

※ 주니어 개발자이기에 부족한 점이 많습니다. 좋은 의견 및 틀린 부분이 있을 시 댓글 남겨주시면 감사하겠습니다.

'ElasticSearch' 카테고리의 다른 글

[ ElasticSearch ] Score 계산과 function_score  (0) 2021.03.07

댓글